use crate::callouts::{callout, CalloutScriptCache, CalloutScriptInfo};
use crate::mdev::{MDev, MDevSysfsData, MDevType};
use anyhow::{anyhow, Result};
use log::{debug, warn};
use std::collections::BTreeMap;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::sync::Mutex;
use std::{env, fs};
use uuid::Uuid;
pub trait Environment: std::fmt::Debug {
fn root(&self) -> &Path;
fn find_script(&self, dev: &MDev) -> Option<CalloutScriptInfo>;
fn as_env(self: Rc<Self>) -> Rc<dyn Environment>;
fn mdev_base(&self) -> PathBuf {
self.root().join("sys/bus/mdev/devices")
}
fn config_base(&self) -> PathBuf {
self.root().join("etc/mdevctl.d")
}
fn parent_base(&self) -> PathBuf {
self.root().join("sys/class/mdev_bus")
}
fn config_scripts_base(&self) -> PathBuf {
self.config_base().join("scripts.d")
}
fn scripts_base(&self) -> PathBuf {
self.root().join("usr/lib/mdevctl/scripts.d")
}
fn callout_dir(&self) -> PathBuf {
self.scripts_base().join("callouts")
}
fn old_callout_dir(&self) -> PathBuf {
self.config_scripts_base().join("callouts")
}
fn callout_dirs(&self) -> Vec<PathBuf> {
vec![self.callout_dir(), self.old_callout_dir()]
}
fn notification_dir(&self) -> PathBuf {
self.scripts_base().join("notifiers")
}
fn old_notification_dir(&self) -> PathBuf {
self.config_scripts_base().join("notifiers")
}
fn notification_dirs(&self) -> Vec<PathBuf> {
vec![self.notification_dir(), self.old_notification_dir()]
}
fn self_check(&self) -> Result<()> {
debug!("checking that the environment is sane");
for dir in [
self.config_base(),
self.callout_dir(),
self.notification_dir(),
] {
if !dir.exists() {
return Err(anyhow!("Required directory {:?} doesn't exist. This may indicate a packaging or installation error", dir));
}
}
Ok(())
}
fn get_active_device(self: Rc<Self>, uuid: Uuid, parent: Option<&String>) -> Result<MDev> {
let devs = self.get_active_devices(Some(&uuid), parent)?;
if devs.is_empty() {
match parent {
None => Err(anyhow!(
"Mediated device {} is not active",
uuid.hyphenated().to_string()
)),
Some(p) => Err(anyhow!(
"Mediated device {}/{} is not active",
p,
uuid.hyphenated().to_string()
)),
}
} else if devs.len() > 1 {
Err(anyhow!(
"Multiple parents found for {}. System error?",
uuid.hyphenated().to_string()
))
} else {
let (parent, children) = devs.iter().next().unwrap();
if children.len() > 1 {
return Err(anyhow!(
"Multiple definitions found for {}/{}",
parent,
uuid.hyphenated().to_string()
));
}
Ok(children.first().unwrap().clone())
}
}
fn get_active_devices(
self: Rc<Self>,
uuid: Option<&Uuid>,
parent: Option<&String>,
) -> Result<BTreeMap<String, Vec<MDev>>> {
let mut devices: BTreeMap<String, Vec<MDev>> = BTreeMap::new();
debug!(
"Looking up active mdevs: uuid={:?}, parent={:?}",
uuid, parent
);
if let Ok(dir) = self.mdev_base().read_dir() {
for dir_dev in dir {
let dir_dev = dir_dev?;
let fname = dir_dev.file_name();
let basename = fname.to_str().unwrap();
debug!("found defined mdev {}", basename);
let u = Uuid::parse_str(basename);
if u.is_err() {
warn!("Can't determine uuid for file '{}'", basename);
continue;
}
let u = u.unwrap();
if uuid.is_some() && uuid != Some(&u) {
debug!(
"Ignoring device {} because it doesn't match uuid {}",
u,
uuid.unwrap()
);
continue;
}
let mut dev = MDev::new(self.clone().as_env(), u);
if let Ok(sysfs_data) = MDevSysfsData::load_for_mdev(&dev) {
dev.set_sysfs_data(sysfs_data);
if dev.active {
if parent.is_some() && (parent != dev.parent.as_ref()) {
debug!(
"Ignoring device {} because it doesn't match parent {}",
dev.uuid,
parent.as_ref().unwrap()
);
continue;
}
let mut per_dev = MDev::new(self.clone().as_env(), u);
per_dev.parent.clone_from(&dev.parent);
if per_dev.load_definition().is_ok() {
dev.autostart = per_dev.autostart;
}
let mut c = callout(&mut dev)?;
if let Ok(attrs) = c.get_attributes() {
let _ = c.dev.add_attributes(&attrs);
}
let devparent = dev.parent()?;
if !devices.contains_key(devparent) {
devices.insert(devparent.clone(), Vec::new());
};
devices.get_mut(devparent).unwrap().push(dev);
};
};
}
}
Ok(devices)
}
fn get_defined_devices(
self: Rc<Self>,
uuid: Option<&Uuid>,
parent: Option<&String>,
) -> Result<BTreeMap<String, Vec<MDev>>> {
let mut devices: BTreeMap<String, Vec<MDev>> = BTreeMap::new();
debug!(
"Looking up defined mdevs: uuid={:?}, parent={:?}",
uuid, parent
);
let thisenv = self.as_env();
for parentpath in thisenv.config_base().read_dir()?.skip_while(|x| match x {
Ok(d) => d.path() == thisenv.scripts_base(),
_ => false,
}) {
let parentpath = parentpath?;
let parentname = parentpath.file_name();
let parentname = parentname.to_str().unwrap();
if (parent.is_some() && parent.unwrap() != parentname)
|| !parentpath.metadata()?.is_dir()
{
debug!("Ignoring child devices for parent {}", parentname);
continue;
}
let mut childdevices = Vec::new();
match parentpath.path().read_dir() {
Ok(res) => {
for child in res {
let child = child?;
match child.metadata() {
Ok(metadata) => {
if !metadata.is_file() {
continue;
}
}
Err(e) => {
warn!("unable to access file {:?}: {}", child.path(), e);
continue;
}
}
let path = child.path();
let basename = path.file_name().unwrap().to_str().unwrap();
let u = Uuid::parse_str(basename);
if u.is_err() {
warn!("Can't determine uuid for file '{}'", basename);
continue;
}
let u = u.unwrap();
debug!("found mdev {:?}", u);
if uuid.is_some() && uuid != Some(&u) {
debug!(
"Ignoring device {} because it doesn't match uuid {}",
u,
uuid.unwrap()
);
continue;
}
match fs::File::open(&path) {
Ok(mut f) => {
let mut contents = String::new();
f.read_to_string(&mut contents)?;
let val = serde_json::from_str(&contents)?;
let mut dev = MDev::new(thisenv.clone(), u);
dev.load_from_json(parentname.to_string(), &val)?;
match MDevSysfsData::load_for_mdev(&dev) {
Err(e) => warn!(
"For device {} a sysfs update caused the error: {:?}",
u, e
),
Ok(Some(sysfs_data)) => {
if dev.sysfs_data_matches(&sysfs_data) {
dev.set_sysfs_data(Some(sysfs_data));
}
}
_ => (),
};
childdevices.push(dev);
}
Err(e) => {
warn!("Unable to open file {:?}: {}", path, e);
continue;
}
};
}
}
Err(e) => warn!("Unable to read directory {:?}: {}", parentpath.path(), e),
}
if !childdevices.is_empty() {
devices.insert(parentname.to_string(), childdevices);
}
}
Ok(devices)
}
fn get_defined_device(self: Rc<Self>, uuid: Uuid, parent: Option<&String>) -> Result<MDev> {
let devs = self.get_defined_devices(Some(&uuid), parent)?;
if devs.is_empty() {
match parent {
None => Err(anyhow!(
"Mediated device {} is not defined",
uuid.hyphenated().to_string()
)),
Some(p) => Err(anyhow!(
"Mediated device {}/{} is not defined",
p,
uuid.hyphenated().to_string()
)),
}
} else if devs.len() > 1 {
match parent {
None => Err(anyhow!(
"Multiple definitions found for {}, specify a parent",
uuid.hyphenated().to_string()
)),
Some(p) => Err(anyhow!(
"Multiple definitions found for {}/{}",
p,
uuid.hyphenated().to_string()
)),
}
} else {
let (parent, children) = devs.iter().next().unwrap();
if children.len() > 1 {
return Err(anyhow!(
"Multiple definitions found for {}/{}",
parent,
uuid.hyphenated().to_string()
));
}
Ok(children.first().unwrap().clone())
}
}
fn get_supported_types(
self: Rc<Self>,
parent: Option<String>,
) -> Result<BTreeMap<String, Vec<MDevType>>> {
debug!("Finding supported mdev types");
let mut types: BTreeMap<String, Vec<MDevType>> = BTreeMap::new();
if let Ok(dir) = self.parent_base().read_dir() {
for parentpath in dir {
let parentpath = parentpath?;
let parentname = parentpath.file_name();
let parentname = parentname.to_str().unwrap();
debug!("Looking for supported types for device {}", parentname);
if parent.is_some() && parent.as_ref().unwrap() != parentname {
debug!("Ignoring types for parent {}", parentname);
continue;
}
let mut childtypes = Vec::new();
let mut parentpath = parentpath.path();
parentpath.push("mdev_supported_types");
for child in parentpath.read_dir()? {
let child = child?;
if !child.metadata()?.is_dir() {
continue;
}
let mut t = MDevType::new();
t.parent = parentname.to_string();
let mut path = child.path();
t.typename = path.file_name().unwrap().to_str().unwrap().to_string();
debug!("found mdev type {}", t.typename);
path.push("available_instances");
debug!("Checking available instances: {:?}", path);
t.available_instances = fs::read_to_string(&path)?.trim().parse()?;
path.pop();
path.push("device_api");
t.device_api = fs::read_to_string(&path)?.trim().to_string();
path.pop();
path.push("name");
if path.exists() {
t.name = fs::read_to_string(&path)?.trim().to_string();
}
path.pop();
path.push("description");
if path.exists() {
t.description = fs::read_to_string(&path)?
.trim()
.replace('\n', ", ")
.to_string();
}
childtypes.push(t);
}
types.insert(parentname.to_string(), childtypes);
}
}
for v in types.values_mut() {
v.sort_by(|a, b| a.typename.cmp(&b.typename));
}
Ok(types)
}
}
#[derive(Debug)]
pub struct DefaultEnvironment {
rootpath: PathBuf,
callout_scripts: Mutex<CalloutScriptCache>,
}
impl Environment for DefaultEnvironment {
fn root(&self) -> &Path {
self.rootpath.as_path()
}
fn find_script(&self, dev: &MDev) -> Option<CalloutScriptInfo> {
return self
.callout_scripts
.lock()
.unwrap()
.find_versioned_script(dev);
}
fn as_env(self: Rc<Self>) -> Rc<dyn Environment> {
self.clone()
}
}
impl DefaultEnvironment {
#[allow(clippy::new_ret_no_self)]
pub fn new() -> Rc<dyn Environment> {
let root = match env::var("MDEVCTL_ENV_ROOT") {
Ok(d) => d,
_ => "/".to_string(),
};
Rc::new(DefaultEnvironment {
rootpath: PathBuf::from(root),
callout_scripts: Mutex::new(CalloutScriptCache::new()),
})
}
}