use std::{
collections::HashMap,
fmt,
fs::OpenOptions,
hash::{Hash, Hasher},
path::{Path, PathBuf},
};
use serde_json::Value;
use devicemapper::Device;
use crate::engine::{
strat_engine::{
backstore::{CryptMetadataHandle, StratBlockDev},
metadata::{static_header, StratisIdentifiers, BDA},
udev::{
block_enumerator, decide_ownership, UdevOwnership, CRYPTO_FS_TYPE, FS_TYPE_KEY,
STRATIS_FS_TYPE,
},
},
types::{EncryptionInfo, Name, PoolUuid, UdevEngineDevice, UdevEngineEvent},
};
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct StratisDevInfo {
pub device_number: Device,
pub devnode: PathBuf,
}
impl<'a> Into<Value> for &'a StratisDevInfo {
fn into(self) -> Value {
json!({
"major": Value::from(self.device_number.major),
"minor": Value::from(self.device_number.minor),
"devnode": Value::from(self.devnode.display().to_string())
})
}
}
impl fmt::Display for StratisDevInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"device number: \"{}\", devnode: \"{}\"",
self.device_number,
self.devnode.display()
)
}
}
#[derive(Debug, Eq, Hash, PartialEq)]
pub struct LuksInfo {
pub dev_info: StratisDevInfo,
pub identifiers: StratisIdentifiers,
pub encryption_info: EncryptionInfo,
pub pool_name: Option<Name>,
}
impl fmt::Display for LuksInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}, {}, {}",
self.dev_info, self.identifiers, self.encryption_info,
)
}
}
#[derive(Debug)]
pub struct StratisInfo {
pub bda: BDA,
pub dev_info: StratisDevInfo,
}
impl PartialEq for StratisInfo {
fn eq(&self, rhs: &Self) -> bool {
self.bda.identifiers() == rhs.bda.identifiers() && self.dev_info == rhs.dev_info
}
}
impl Eq for StratisInfo {}
impl Hash for StratisInfo {
fn hash<H>(&self, hasher: &mut H)
where
H: Hasher,
{
self.bda.identifiers().hash(hasher);
self.dev_info.hash(hasher);
}
}
impl fmt::Display for StratisInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}, {}", self.bda.identifiers(), self.dev_info,)
}
}
#[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.identifiers,
DeviceInfo::Stratis(info) => info.bda.identifiers(),
}
}
pub fn encryption_info(&self) -> Option<&EncryptionInfo> {
match self {
DeviceInfo::Luks(info) => Some(&info.encryption_info),
DeviceInfo::Stratis(_) => None,
}
}
}
impl From<StratBlockDev> for DeviceInfo {
fn from(bd: StratBlockDev) -> Self {
match (bd.encryption_info(), bd.pool_name(), bd.luks_device()) {
(Some(ei), Some(pname), Some(dev)) => DeviceInfo::Luks(LuksInfo {
encryption_info: ei.clone(),
dev_info: StratisDevInfo {
device_number: *dev,
devnode: bd.physical_path().to_owned(),
},
identifiers: StratisIdentifiers {
pool_uuid: bd.pool_uuid(),
device_uuid: bd.uuid(),
},
pool_name: pname.cloned(),
}),
(None, None, None) => DeviceInfo::Stratis(StratisInfo {
dev_info: StratisDevInfo {
device_number: *bd.device(),
devnode: bd.physical_path().to_owned(),
},
bda: bd.bda,
}),
(_, _, _) => unreachable!("If bd.is_encrypted(), all are Some(_)"),
}
}
}
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)
}
pub fn bda_wrapper(devnode: &Path) -> Result<Result<Option<BDA>, 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| {
let header =
static_header(f).map_err(|_| "failed to read static header".to_string())?;
match header {
Some(h) => Ok(BDA::load(h, f)
.map_err(|_| "failed to read MDA or MDA was invalid".to_string())?),
None => Ok(None),
}
})
}
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 CryptMetadataHandle::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 {
dev_info: StratisDevInfo {
device_number,
devnode: handle.luks2_device_path().to_path_buf(),
},
identifiers: *handle.device_identifiers(),
encryption_info: handle.encryption_info().to_owned(),
pool_name: handle.pool_name().cloned(),
}),
},
},
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), bda_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(bda)))) => Some(StratisInfo {
bda,
dev_info: StratisDevInfo {
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.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.bda.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::error::Error;
use crate::{
engine::{
strat_engine::{
backstore::{initialize_devices, ProcessedPathInfos, UnownedDevices},
cmd::create_fs,
metadata::MDADataSize,
tests::{crypt, loopbacked, real},
udev::block_device_apply,
},
types::{DevicePath, EncryptionInfo, KeyDescription},
},
stratis::{StratisError, StratisResult},
};
use super::*;
fn get_devices(paths: &[&Path]) -> StratisResult<UnownedDevices> {
ProcessedPathInfos::try_from(paths)
.map(|ps| ps.unpack())
.map(|(sds, uds)| {
sds.error_on_not_empty().unwrap();
uds
})
}
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 pool_name = Name::new("pool_name".to_string());
let devices = initialize_devices(
get_devices(paths)?,
pool_name,
pool_uuid,
MDADataSize::default(),
Some(&EncryptionInfo::KeyDesc(key_description.clone())),
)?;
for dev in devices {
let info =
block_device_apply(&DevicePath::new(dev.physical_path()).unwrap(), |dev| {
process_luks_device(dev)
})?
.ok_or_else(|| {
StratisError::Msg(
"No device with specified devnode found in udev database".into(),
)
})?
.ok_or_else(|| {
StratisError::Msg(
"No LUKS information for Stratis found on specified device".into(),
)
})?;
if info.identifiers.pool_uuid != pool_uuid {
return Err(Box::new(StratisError::Msg(format!(
"Discovered pool UUID {} != expected pool UUID {}",
info.identifiers.pool_uuid, pool_uuid
))));
}
if info.dev_info.devnode != dev.physical_path() {
return Err(Box::new(StratisError::Msg(format!(
"Discovered device node {} != expected device node {}",
info.dev_info.devnode.display(),
dev.physical_path().display()
))));
}
if info.encryption_info.key_description() != Some(key_description) {
return Err(Box::new(StratisError::Msg(format!(
"Discovered key description {:?} != expected key description {:?}",
info.encryption_info.key_description(),
Some(key_description.as_application_str())
))));
}
let info =
block_device_apply(&DevicePath::new(dev.physical_path()).unwrap(), |dev| {
process_stratis_device(dev)
})?
.ok_or_else(|| {
StratisError::Msg(
"No device with specified devnode found in udev database".into(),
)
})?;
if info.is_some() {
return Err(Box::new(StratisError::Msg(
"Encrypted block device was incorrectly identified as a Stratis device"
.to_string(),
)));
}
let info =
block_device_apply(&DevicePath::new(dev.metadata_path()).unwrap(), |dev| {
process_stratis_device(dev)
})?
.ok_or_else(|| {
StratisError::Msg(
"No device with specified devnode found in udev database".into(),
)
})?
.ok_or_else(|| {
StratisError::Msg("No Stratis metadata found on specified device".into())
})?;
if info.bda.identifiers().pool_uuid != pool_uuid
|| info.dev_info.devnode != dev.metadata_path()
{
return Err(Box::new(StratisError::Msg(format!(
"Wrong identifiers and devnode found on Stratis block device: found: pool UUID: {}, device node; {} != expected: pool UUID: {}, device node: {}",
info.bda.identifiers().pool_uuid,
info.dev_info.devnode.display(),
pool_uuid,
dev.metadata_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();
let pool_name = Name::new("pool_name".to_string());
initialize_devices(
get_devices(paths).unwrap(),
pool_name,
pool_uuid,
MDADataSize::default(),
None,
)
.unwrap();
for path in paths {
let device_path = DevicePath::new(path).expect("our test path");
let info = block_device_apply(&device_path, process_stratis_device)
.unwrap()
.unwrap()
.unwrap();
assert_eq!(info.bda.identifiers().pool_uuid, pool_uuid);
assert_eq!(&&info.dev_info.devnode, path);
assert_eq!(
block_device_apply(&device_path, process_luks_device)
.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 {
let device_path = DevicePath::new(path).expect("our test path");
assert_eq!(
block_device_apply(&device_path, process_stratis_device)
.unwrap()
.unwrap(),
None
);
assert_eq!(
block_device_apply(&device_path, process_luks_device)
.unwrap()
.unwrap(),
None
);
}
for path in paths {
create_fs(path, None, false).unwrap();
let device_path = DevicePath::new(path).expect("our test path");
assert_eq!(
block_device_apply(&device_path, process_stratis_device)
.unwrap()
.unwrap(),
None
);
assert_eq!(
block_device_apply(&device_path, process_luks_device)
.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,
);
}
}