use std::{
collections::HashMap,
fmt,
fs::OpenOptions,
path::{Path, PathBuf},
};
use serde_json::Value;
use devicemapper::Device;
use crate::engine::{
strat_engine::{
backstore::CryptHandle,
metadata::{device_identifiers, StratisIdentifiers},
udev::{
block_enumerator, decide_ownership, UdevOwnership, CRYPTO_FS_TYPE, FS_TYPE_KEY,
STRATIS_FS_TYPE,
},
},
types::{EncryptionInfo, PoolUuid, UdevEngineDevice, UdevEngineEvent},
};
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct LuksInfo {
pub info: StratisInfo,
pub encryption_info: EncryptionInfo,
}
impl fmt::Display for LuksInfo {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}, {}", self.info, self.encryption_info,)
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct StratisInfo {
pub identifiers: StratisIdentifiers,
pub device_number: Device,
pub devnode: PathBuf,
}
impl fmt::Display for StratisInfo {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}, device number: \"{}\", devnode: \"{}\"",
self.identifiers,
self.device_number,
self.devnode.display()
)
}
}
impl<'a> Into<Value> for &'a StratisInfo {
fn into(self) -> Value {
let mut json = json!({
"major": Value::from(self.device_number.major),
"minor": Value::from(self.device_number.minor),
"devnode": Value::from(self.devnode.display().to_string())
});
if let Value::Object(ref mut map) = json {
map.extend(
if let Value::Object(map) =
<&StratisIdentifiers as Into<Value>>::into(&self.identifiers)
{
map.into_iter()
} else {
unreachable!("StratisIdentifiers conversion returns a JSON object");
},
);
} else {
unreachable!("json!() always creates a JSON object")
};
json
}
}
#[derive(Debug, Eq, Hash, PartialEq)]
pub enum DeviceInfo {
Luks(LuksInfo),
Stratis(StratisInfo),
}
impl DeviceInfo {
pub fn stratis_identifiers(&self) -> StratisIdentifiers {
match self {
DeviceInfo::Luks(info) => info.info.identifiers,
DeviceInfo::Stratis(info) => info.identifiers,
}
}
pub fn encryption_info(&self) -> Option<&EncryptionInfo> {
match self {
DeviceInfo::Luks(info) => Some(&info.encryption_info),
DeviceInfo::Stratis(_) => None,
}
}
}
impl fmt::Display for DeviceInfo {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
DeviceInfo::Luks(info) => write!(f, "LUKS device description: {}", info),
DeviceInfo::Stratis(info) => write!(f, "Stratis device description: {}", info),
}
}
}
fn device_to_devno_wrapper(device: &UdevEngineDevice) -> Result<Device, String> {
device
.devnum()
.ok_or_else(|| "udev entry did not contain a device number".into())
.map(Device::from)
}
fn device_identifiers_wrapper(
devnode: &Path,
) -> Result<Result<Option<StratisIdentifiers>, String>, String> {
OpenOptions::new()
.read(true)
.open(devnode)
.as_mut()
.map_err(|err| {
format!(
"device {} could not be opened for reading: {}",
devnode.display(),
err
)
})
.map(|f| {
device_identifiers(f).map_err(|err| {
format!(
"encountered an error while reading Stratis header for device {}: {}",
devnode.display(),
err
)
})
})
}
fn process_luks_device(dev: &UdevEngineDevice) -> Option<LuksInfo> {
match dev.devnode() {
Some(devnode) => match device_to_devno_wrapper(dev) {
Err(err) => {
warn!(
"udev identified device {} as a Stratis device but {}, disregarding the device",
devnode.display(),
err
);
None
}
Ok(device_number) => match CryptHandle::setup(devnode) {
Ok(None) => None,
Err(err) => {
warn!(
"udev identified device {} as a LUKS device, but could not read LUKS header from the device, disregarding the device: {}",
devnode.display(),
err,
);
None
}
Ok(Some(handle)) => Some(LuksInfo {
info: StratisInfo {
identifiers: *handle.device_identifiers(),
device_number,
devnode: handle.luks2_device_path().to_path_buf(),
},
encryption_info: handle.encryption_info().to_owned(),
}),
},
},
None => {
warn!("udev identified a device as a LUKS2 device, but the udev entry for the device had no device node, disregarding device");
None
}
}
}
fn process_stratis_device(dev: &UdevEngineDevice) -> Option<StratisInfo> {
match dev.devnode() {
Some(devnode) => {
match (
device_to_devno_wrapper(dev),
device_identifiers_wrapper(devnode),
) {
(Err(err), _) | (_, Err(err)) | (_, Ok(Err(err))) => {
warn!("udev identified device {} as a Stratis device but {}, disregarding the device",
devnode.display(),
err);
None
}
(_, Ok(Ok(None))) => {
warn!("udev identified device {} as a Stratis device but there appeared to be no Stratis metadata on the device, disregarding the device",
devnode.display());
None
}
(Ok(device_number), Ok(Ok(Some(identifiers)))) => Some(StratisInfo {
identifiers,
device_number,
devnode: devnode.to_path_buf(),
}),
}
}
None => {
warn!("udev identified a device as a Stratis device, but the udev entry for the device had no device node, disregarding device");
None
}
}
}
fn find_all_luks_devices() -> libudev::Result<HashMap<PoolUuid, Vec<LuksInfo>>> {
let context = libudev::Context::new()?;
let mut enumerator = block_enumerator(&context)?;
enumerator.match_property(FS_TYPE_KEY, CRYPTO_FS_TYPE)?;
let pool_map = enumerator
.scan_devices()?
.filter_map(|dev| identify_luks_device(&UdevEngineDevice::from(&dev)))
.fold(HashMap::new(), |mut acc, info| {
acc.entry(info.info.identifiers.pool_uuid)
.or_insert_with(Vec::new)
.push(info);
acc
});
Ok(pool_map)
}
fn find_all_stratis_devices() -> libudev::Result<HashMap<PoolUuid, Vec<StratisInfo>>> {
let context = libudev::Context::new()?;
let mut enumerator = block_enumerator(&context)?;
enumerator.match_property(FS_TYPE_KEY, STRATIS_FS_TYPE)?;
let pool_map = enumerator
.scan_devices()?
.filter_map(|dev| identify_stratis_device(&UdevEngineDevice::from(&dev)))
.fold(HashMap::new(), |mut acc, info| {
acc.entry(info.identifiers.pool_uuid)
.or_insert_with(Vec::new)
.push(info);
acc
});
Ok(pool_map)
}
fn identify_luks_device(dev: &UdevEngineDevice) -> Option<LuksInfo> {
let initialized = dev.is_initialized();
if !initialized {
warn!("Found a udev entry for a device identified as a Stratis device, but udev also identified it as uninitialized, disregarding the device");
return None;
};
match decide_ownership(dev) {
Err(err) => {
warn!("Could not determine ownership of a block device identified as a LUKS device by udev, disregarding the device: {}",
err);
None
}
Ok(ownership) => match ownership {
UdevOwnership::Luks => process_luks_device(dev),
UdevOwnership::MultipathMember => None,
_ => {
warn!("udev enumeration identified this device as a LUKS block device but on further examination udev identifies it as a {}",
ownership);
None
}
},
}
.map(|info| {
info!("LUKS block device belonging to Stratis with {} discovered during initial search",
info,
);
info
})
}
fn identify_stratis_device(dev: &UdevEngineDevice) -> Option<StratisInfo> {
let initialized = dev.is_initialized();
if !initialized {
warn!("Found a udev entry for a device identified as a Stratis device, but udev also identified it as uninitialized, disregarding the device");
return None;
};
match decide_ownership(dev) {
Err(err) => {
warn!("Could not determine ownership of a block device identified as a Stratis device by udev, disregarding the device: {}",
err);
None
}
Ok(ownership) => match ownership {
UdevOwnership::Stratis => process_stratis_device(dev),
UdevOwnership::MultipathMember => None,
_ => {
warn!("udev enumeration identified this device as a Stratis block device but on further examination udev identifies it as a {}",
ownership);
None
}
},
}
.map(|info| {
info!("Stratis block device with {} discovered during initial search",
info,
);
info
})
}
pub fn identify_block_device(event: &UdevEngineEvent) -> Option<DeviceInfo> {
let initialized = event.device().is_initialized();
if !initialized {
debug!("Found a udev entry for a device identified as a block device, but udev also identified it as uninitialized, disregarding the device");
return None;
};
match decide_ownership(event.device()) {
Err(err) => {
warn!(
"Could not determine ownership of a udev block device, disregarding the device: {}",
err
);
None
}
Ok(ownership) => match ownership {
UdevOwnership::Stratis => {
process_stratis_device(event.device()).map(DeviceInfo::Stratis)
}
UdevOwnership::Luks => process_luks_device(event.device()).map(DeviceInfo::Luks),
_ => None,
},
}
.map(|info| {
debug!("Stratis block device with {} identified", info);
info
})
}
#[allow(clippy::type_complexity)]
pub fn find_all() -> libudev::Result<(
HashMap<PoolUuid, Vec<LuksInfo>>,
HashMap<PoolUuid, Vec<StratisInfo>>,
)> {
info!("Beginning initial search for Stratis block devices");
find_all_luks_devices()
.and_then(|luks| find_all_stratis_devices().map(|stratis| (luks, stratis)))
}
#[cfg(test)]
mod tests {
use std::{collections::HashSet, error::Error};
use crate::{
engine::{
strat_engine::{
backstore::{initialize_devices, process_and_verify_devices},
cmd::create_fs,
metadata::MDADataSize,
tests::{crypt, loopbacked, real},
udev::block_device_apply,
},
types::{EncryptionInfo, KeyDescription},
BlockDev,
},
stratis::StratisError,
};
use super::*;
fn test_process_luks_device_initialized(paths: &[&Path]) {
assert!(!paths.is_empty());
fn luks_device_test(
paths: &[&Path],
key_description: &KeyDescription,
_: (),
) -> Result<(), Box<dyn Error>> {
let pool_uuid = PoolUuid::new_v4();
let devices = initialize_devices(
process_and_verify_devices(pool_uuid, &HashSet::new(), paths)?,
pool_uuid,
MDADataSize::default(),
&EncryptionInfo {
key_description: Some(key_description.clone()),
clevis_info: None,
},
)?;
for dev in devices {
let info = block_device_apply(dev.physical_path(), |dev| process_luks_device(dev))?
.ok_or_else(|| {
StratisError::Error(
"No device with specified devnode found in udev database".into(),
)
})?
.ok_or_else(|| {
StratisError::Error(
"No LUKS information for Stratis found on specified device".into(),
)
})?;
if info.info.identifiers.pool_uuid != pool_uuid {
return Err(Box::new(StratisError::Error(format!(
"Discovered pool UUID {} != expected pool UUID {}",
info.info.identifiers.pool_uuid, pool_uuid
))));
}
if info.info.devnode != dev.physical_path() {
return Err(Box::new(StratisError::Error(format!(
"Discovered device node {} != expected device node {}",
info.info.devnode.display(),
dev.physical_path().display()
))));
}
if info.encryption_info.key_description.as_ref() != Some(key_description) {
return Err(Box::new(StratisError::Error(format!(
"Discovered key description {:?} != expected key description {:?}",
info.encryption_info.key_description,
Some(key_description.as_application_str())
))));
}
let info =
block_device_apply(dev.physical_path(), |dev| process_stratis_device(dev))?
.ok_or_else(|| {
StratisError::Error(
"No device with specified devnode found in udev database".into(),
)
})?;
if info.is_some() {
return Err(Box::new(StratisError::Error(
"Encrypted block device was incorrectly identified as a Stratis device"
.to_string(),
)));
}
let info =
block_device_apply(&dev.user_path()?, |dev| process_stratis_device(dev))?
.ok_or_else(|| {
StratisError::Error(
"No device with specified devnode found in udev database".into(),
)
})?
.ok_or_else(|| {
StratisError::Error(
"No Stratis metadata found on specified device".into(),
)
})?;
if info.identifiers.pool_uuid != pool_uuid || info.devnode != dev.user_path()? {
return Err(Box::new(StratisError::Error(format!(
"Wrong identifiers and devnode found on Stratis block device: found: pool UUID: {}, device node; {} != expected: pool UUID: {}, device node: {}",
info.identifiers.pool_uuid,
info.devnode.display(),
pool_uuid,
dev.user_path()?.display()),
)));
}
}
Ok(())
}
crypt::insert_and_cleanup_key(paths, luks_device_test);
}
#[test]
fn loop_test_process_luks_device_initialized() {
loopbacked::test_with_spec(
&loopbacked::DeviceLimits::Exactly(1, None),
test_process_luks_device_initialized,
);
}
#[test]
fn real_test_process_luks_device_initialized() {
real::test_with_spec(
&real::DeviceLimits::Exactly(1, None, None),
test_process_luks_device_initialized,
);
}
fn test_process_device_initialized(paths: &[&Path]) {
assert!(!paths.is_empty());
let pool_uuid = PoolUuid::new_v4();
initialize_devices(
process_and_verify_devices(pool_uuid, &HashSet::new(), paths).unwrap(),
pool_uuid,
MDADataSize::default(),
&EncryptionInfo::default(),
)
.unwrap();
for path in paths {
let info = block_device_apply(path, |dev| process_stratis_device(dev))
.unwrap()
.unwrap()
.unwrap();
assert_eq!(info.identifiers.pool_uuid, pool_uuid);
assert_eq!(&&info.devnode, path);
assert_eq!(
block_device_apply(path, |dev| process_luks_device(dev))
.unwrap()
.unwrap(),
None
);
}
}
#[test]
fn loop_test_process_device_initialized() {
loopbacked::test_with_spec(
&loopbacked::DeviceLimits::Exactly(1, None),
test_process_device_initialized,
);
}
#[test]
fn real_test_process_device_initialized() {
real::test_with_spec(
&real::DeviceLimits::Exactly(1, None, None),
test_process_device_initialized,
);
}
fn test_process_device_uninitialized(paths: &[&Path]) {
assert!(!paths.is_empty());
for path in paths {
assert_eq!(
block_device_apply(path, |dev| process_stratis_device(dev))
.unwrap()
.unwrap(),
None
);
assert_eq!(
block_device_apply(path, |dev| process_luks_device(dev))
.unwrap()
.unwrap(),
None
);
}
for path in paths {
create_fs(path, None, false).unwrap();
assert_eq!(
block_device_apply(path, |dev| process_stratis_device(dev))
.unwrap()
.unwrap(),
None
);
assert_eq!(
block_device_apply(path, |dev| process_luks_device(dev))
.unwrap()
.unwrap(),
None
);
}
}
#[test]
fn loop_test_process_device_uninitialized() {
loopbacked::test_with_spec(
&loopbacked::DeviceLimits::Exactly(1, None),
test_process_device_uninitialized,
);
}
#[test]
fn real_test_process_device_uninitialized() {
real::test_with_spec(
&real::DeviceLimits::Exactly(1, None, None),
test_process_device_uninitialized,
);
}
}