diff --git a/dracut/90stratis-clevis/stratis-clevis-rootfs-setup b/dracut/90stratis-clevis/stratis-clevis-rootfs-setup index 846b0152a2..ae06c46b0b 100755 --- a/dracut/90stratis-clevis/stratis-clevis-rootfs-setup +++ b/dracut/90stratis-clevis/stratis-clevis-rootfs-setup @@ -6,7 +6,7 @@ if [ -z "$STRATIS_ROOTFS_UUID" ]; then fi i=0 -while ! stratis-min pool is-locked "$STRATIS_ROOTFS_UUID" >/dev/null; do +while ! stratis-min pool is-stopped "$STRATIS_ROOTFS_UUID" >/dev/null; do echo Waiting on pool with UUID $STRATIS_ROOTFS_UUID... sleep 1 if [ "$i" = 5 ]; then @@ -15,10 +15,10 @@ while ! stratis-min pool is-locked "$STRATIS_ROOTFS_UUID" >/dev/null; do i=$(($i + 1)) done -if $(stratis-min pool is-locked "$STRATIS_ROOTFS_UUID"); then +if $(stratis-min pool is-stopped "$STRATIS_ROOTFS_UUID"); then if $(stratis-min pool is-bound "$STRATIS_ROOTFS_UUID"); then - if ! stratis-min pool unlock clevis "$STRATIS_ROOTFS_UUID"; then - echo Failed to unlock pool with UUID $STRATIS_ROOTFS_UUID using Clevis >&2 + if ! stratis-min pool start --unlock-method=clevis "$STRATIS_ROOTFS_UUID"; then + echo Failed to start pool with UUID $STRATIS_ROOTFS_UUID using Clevis >&2 exit 1 fi fi diff --git a/dracut/90stratis/stratis-rootfs-setup b/dracut/90stratis/stratis-rootfs-setup index 3eb8da68a0..e14509dd6e 100755 --- a/dracut/90stratis/stratis-rootfs-setup +++ b/dracut/90stratis/stratis-rootfs-setup @@ -6,7 +6,7 @@ if [ -z "$STRATIS_ROOTFS_UUID" ]; then fi i=0 -while ! stratis-min pool is-locked "$STRATIS_ROOTFS_UUID" >/dev/null; do +while ! stratis-min pool is-stopped "$STRATIS_ROOTFS_UUID" >/dev/null; do echo Waiting on pool with UUID $STRATIS_ROOTFS_UUID... sleep 1 if [ "$i" = 5 ]; then @@ -15,12 +15,19 @@ while ! stratis-min pool is-locked "$STRATIS_ROOTFS_UUID" >/dev/null; do i=$(($i + 1)) done -if $(stratis-min pool is-locked "$STRATIS_ROOTFS_UUID"); then - if ! plymouth ask-for-password \ - --command="stratis-min pool unlock --prompt keyring $STRATIS_ROOTFS_UUID" \ - --prompt="Enter password for Stratis pool with UUID $STRATIS_ROOTFS_UUID containing root filesystem" \ - --number-of-tries=3; then - echo Failed to unlock pool with UUID $STRATIS_ROOTFS_UUID using a passphrase >&2 - exit 1 +if $(stratis-min pool is-stopped "$STRATIS_ROOTFS_UUID"); then + if $(stratis-min pool is-encrypted "$STRATIS_ROOTFS_UUID"); then + if ! plymouth ask-for-password \ + --command="stratis-min pool start --prompt --unlock-method=keyring $STRATIS_ROOTFS_UUID" \ + --prompt="Enter password for Stratis pool with UUID $STRATIS_ROOTFS_UUID containing root filesystem" \ + --number-of-tries=3; then + echo Failed to start pool with UUID $STRATIS_ROOTFS_UUID using a passphrase >&2 + exit 1 + fi + else + if ! stratis-min pool start "$STRATIS_ROOTFS_UUID"; then + echo Failed to start pool with UUID $STRATIS_ROOTFS_UUID >&2 + exit 1 + fi fi fi diff --git a/src/bin/stratis-min/stratis-min.rs b/src/bin/stratis-min/stratis-min.rs index ac43d84aed..ed82ae2ade 100644 --- a/src/bin/stratis-min/stratis-min.rs +++ b/src/bin/stratis-min/stratis-min.rs @@ -48,15 +48,20 @@ fn parse_args() -> Command<'static> { Command::new("unset").arg(Arg::new("key_desc").required(true)), ]), Command::new("pool").subcommands(vec![ - Command::new("unlock") - .arg(Arg::new("unlock_method").required(true)) - .arg(Arg::new("pool_uuid").required(false)) + Command::new("start") + .arg(Arg::new("pool_uuid").required(true)) + .arg( + Arg::new("unlock_method") + .long("--unlock-method") + .takes_value(true), + ) .arg( Arg::new("prompt") .long("--prompt") .takes_value(false) - .requires("pool_uuid"), + .requires("unlock_method"), ), + Command::new("stop").arg(Arg::new("pool_uuid").required(true)), Command::new("create") .arg(Arg::new("name").required(true)) .arg( @@ -117,7 +122,7 @@ fn parse_args() -> Command<'static> { ), Command::new("destroy").arg(Arg::new("name").required(true)), Command::new("is-encrypted").arg(Arg::new("pool_uuid").required(true)), - Command::new("is-locked").arg(Arg::new("pool_uuid").required(true)), + Command::new("is-stopped").arg(Arg::new("pool_uuid").required(true)), Command::new("is-bound").arg(Arg::new("pool_uuid").required(true)), Command::new("has-passphrase").arg(Arg::new("pool_uuid").required(true)), Command::new("clevis-pin").arg(Arg::new("pool_uuid").required(true)), @@ -169,20 +174,23 @@ fn main() -> Result<(), String> { Ok(()) } } else if let Some(subcommand) = args.subcommand_matches("pool") { - if let Some(args) = subcommand.subcommand_matches("unlock") { - let unlock_method = - UnlockMethod::try_from(args.value_of("unlock_method").expect("required"))?; - let uuid = match args.value_of("pool_uuid") { - Some(u) => Some(PoolUuid::parse_str(u)?), + if let Some(args) = subcommand.subcommand_matches("start") { + let uuid = PoolUuid::parse_str(args.value_of("pool_uuid").expect("required"))?; + let unlock_method = match args.value_of("unlock_method") { + Some(um) => Some(UnlockMethod::try_from(um)?), None => None, }; let prompt = args.is_present("prompt"); - if prompt && unlock_method == UnlockMethod::Clevis { + if prompt && unlock_method == Some(UnlockMethod::Clevis) { return Err(Box::new(StratisError::Msg( "--prompt and an unlock_method of clevis are mutally exclusive".to_string(), ))); } - pool::pool_unlock(unlock_method, uuid, prompt)?; + pool::pool_start(uuid, unlock_method, prompt)?; + Ok(()) + } else if let Some(args) = subcommand.subcommand_matches("stop") { + let uuid = PoolUuid::parse_str(args.value_of("pool_uuid").expect("required"))?; + pool::pool_stop(uuid)?; Ok(()) } else if let Some(args) = subcommand.subcommand_matches("create") { let paths = get_paths_from_args(args); @@ -241,10 +249,10 @@ fn main() -> Result<(), String> { let uuid = PoolUuid::parse_str(uuid_str)?; println!("{}", pool::pool_is_encrypted(uuid)?,); Ok(()) - } else if let Some(args) = subcommand.subcommand_matches("is-locked") { + } else if let Some(args) = subcommand.subcommand_matches("is-stopped") { let uuid_str = args.value_of("pool_uuid").expect("required"); let uuid = PoolUuid::parse_str(uuid_str)?; - println!("{}", pool::pool_is_locked(uuid)?,); + println!("{}", pool::pool_is_stopped(uuid)?,); Ok(()) } else if let Some(args) = subcommand.subcommand_matches("is-bound") { let uuid_str = args.value_of("pool_uuid").expect("required"); diff --git a/src/dbus_api/api/manager_3_0/api.rs b/src/dbus_api/api/manager_3_0/api.rs index 5be7f9d4d6..c030fe4bd2 100644 --- a/src/dbus_api/api/manager_3_0/api.rs +++ b/src/dbus_api/api/manager_3_0/api.rs @@ -14,7 +14,7 @@ use crate::{ }, props::{get_locked_pools, get_version}, }, - prop_conv::LockedPools, + prop_conv::StoppedOrLockedPools, }, consts, types::TData, @@ -180,7 +180,7 @@ pub fn locked_pools_property( where E: 'static + Engine, { - f.property::(consts::LOCKED_POOLS_PROP, ()) + f.property::(consts::LOCKED_POOLS_PROP, ()) .access(Access::Read) .emits_changed(EmitsChangedSignal::True) .on_get(get_locked_pools) diff --git a/src/dbus_api/api/manager_3_2/api.rs b/src/dbus_api/api/manager_3_2/api.rs new file mode 100644 index 0000000000..d2cd7f3eac --- /dev/null +++ b/src/dbus_api/api/manager_3_2/api.rs @@ -0,0 +1,82 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use dbus_tree::{Access, EmitsChangedSignal, Factory, MTSync, Method, Property}; + +use crate::{ + dbus_api::{ + api::{ + manager_3_2::{ + methods::{refresh_state, start_pool, stop_pool}, + props::get_stopped_pools, + }, + prop_conv::StoppedOrLockedPools, + }, + consts, + types::TData, + }, + engine::Engine, +}; + +pub fn start_pool_method( + f: &Factory>, TData>, +) -> Method>, TData> +where + E: 'static + Engine, +{ + f.method("StartPool", (), start_pool) + .in_arg(("pool_uuid", "s")) + .in_arg(("unlock_method", "(bs)")) + // In order from left to right: + // b: true if the pool was newly started + // o: pool path + // oa: block device paths + // oa: filesystem paths + // + // Rust representation: bool + .out_arg(("result", "(b(oaoao))")) + .out_arg(("return_code", "q")) + .out_arg(("return_string", "s")) +} + +pub fn stop_pool_method( + f: &Factory>, TData>, +) -> Method>, TData> +where + E: 'static + Engine, +{ + f.method("StopPool", (), stop_pool) + .in_arg(("pool", "o")) + // In order from left to right: + // b: true if the pool was newly stopped + // s: string representation of UUID of stopped pool + // + // Rust representation: (bool, String) + .out_arg(("result", "(bs)")) + .out_arg(("return_code", "q")) + .out_arg(("return_string", "s")) +} + +pub fn refresh_state_method( + f: &Factory>, TData>, +) -> Method>, TData> +where + E: 'static + Engine, +{ + f.method("RefreshState", (), refresh_state) + .out_arg(("return_code", "q")) + .out_arg(("return_string", "s")) +} + +pub fn stopped_pools_property( + f: &Factory>, TData>, +) -> Property>, TData> +where + E: 'static + Engine, +{ + f.property::(consts::STOPPED_POOLS_PROP, ()) + .access(Access::Read) + .emits_changed(EmitsChangedSignal::True) + .on_get(get_stopped_pools) +} diff --git a/src/dbus_api/api/manager_3_2/methods.rs b/src/dbus_api/api/manager_3_2/methods.rs new file mode 100644 index 0000000000..86465b1d4e --- /dev/null +++ b/src/dbus_api/api/manager_3_2/methods.rs @@ -0,0 +1,208 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use std::convert::TryFrom; + +use dbus::{Message, Path}; +use dbus_tree::{MTSync, MethodInfo, MethodResult}; +use futures::executor::block_on; + +use crate::{ + dbus_api::{ + blockdev::create_dbus_blockdev, + consts, + filesystem::create_dbus_filesystem, + pool::create_dbus_pool, + types::{DbusErrorEnum, TData, OK_STRING}, + util::{engine_to_dbus_err_tuple, get_next_arg, tuple_to_option}, + }, + engine::{Engine, LockKey, Pool, PoolUuid, StartAction, StopAction, UnlockMethod}, + stratis::StratisError, +}; + +pub fn start_pool(m: &MethodInfo<'_, MTSync>, TData>) -> MethodResult +where + E: 'static + Engine, +{ + let base_path = m.path.get_name(); + let message: &Message = m.msg; + let mut iter = message.iter_init(); + let dbus_context = m.tree.get_data(); + let default_return: ( + bool, + (Path<'static>, Vec>, Vec>), + ) = (false, (Path::default(), Vec::new(), Vec::new())); + let return_message = message.method_return(); + + let pool_uuid_str: &str = get_next_arg(&mut iter, 0)?; + let pool_uuid = match PoolUuid::parse_str(pool_uuid_str) { + Ok(uuid) => uuid, + Err(e) => { + let e = StratisError::Chained( + "Malformed UUID passed to StartPool".to_string(), + Box::new(e), + ); + let (rc, rs) = engine_to_dbus_err_tuple(&e); + return Ok(vec![return_message.append3(default_return, rc, rs)]); + } + }; + let unlock_method = { + let unlock_method_tup: (bool, &str) = get_next_arg(&mut iter, 1)?; + match tuple_to_option(unlock_method_tup) { + Some(unlock_method_str) => match UnlockMethod::try_from(unlock_method_str) { + Ok(um) => Some(um), + Err(e) => { + let (rc, rs) = engine_to_dbus_err_tuple(&e); + return Ok(vec![return_message.append3(default_return, rc, rs)]); + } + }, + None => None, + } + }; + + let ret = match handle_action!(block_on( + dbus_context.engine.start_pool(pool_uuid, unlock_method,) + )) { + Ok(StartAction::Started(_)) => { + let guard = match block_on(dbus_context.engine.get_pool(LockKey::Uuid(pool_uuid))) { + Some(g) => g, + None => { + let (rc, rs) = engine_to_dbus_err_tuple(&StratisError::Msg( + format!("Pool with UUID {} was successfully started but appears to have been removed before it could be exposed on the D-Bus", pool_uuid) + )); + return Ok(vec![return_message.append3(default_return, rc, rs)]); + } + }; + + let (pool_name, _, pool) = guard.as_tuple(); + let pool_path = + create_dbus_pool(dbus_context, base_path.clone(), &pool_name, pool_uuid, pool); + let mut bd_paths = Vec::new(); + for (bd_uuid, tier, bd) in pool.blockdevs() { + bd_paths.push(create_dbus_blockdev( + dbus_context, + pool_path.clone(), + bd_uuid, + tier, + bd, + )); + } + let mut fs_paths = Vec::new(); + for (name, fs_uuid, fs) in pool.filesystems() { + fs_paths.push(create_dbus_filesystem( + dbus_context, + pool_path.clone(), + &pool_name, + &name, + fs_uuid, + fs, + )); + } + + if pool.is_encrypted() { + dbus_context.push_locked_pools(block_on(dbus_context.engine.locked_pools())); + } + dbus_context.push_stopped_pools(block_on(dbus_context.engine.stopped_pools())); + + (true, (pool_path, bd_paths, fs_paths)) + } + Ok(StartAction::Identity) => default_return, + Err(e) => { + let (rc, rs) = engine_to_dbus_err_tuple(&e); + return Ok(vec![return_message.append3(default_return, rc, rs)]); + } + }; + + Ok(vec![return_message.append3( + ret, + DbusErrorEnum::OK as u16, + OK_STRING.to_string(), + )]) +} + +pub fn stop_pool(m: &MethodInfo<'_, MTSync>, TData>) -> MethodResult +where + E: 'static + Engine, +{ + let message: &Message = m.msg; + let mut iter = message.iter_init(); + let dbus_context = m.tree.get_data(); + let default_return = (false, String::new()); + let return_message = message.method_return(); + + let pool_path: dbus::Path<'static> = get_next_arg(&mut iter, 0)?; + let pool_uuid = match m + .tree + .get(&pool_path) + .and_then(|op| op.get_data().as_ref()) + .map(|d| &d.uuid) + { + Some(uuid) => *typed_uuid!(uuid; Pool; default_return; return_message), + None => { + return Ok(vec![return_message.append3( + default_return, + DbusErrorEnum::OK as u16, + OK_STRING.to_string(), + )]); + } + }; + + // If Some(_), send a locked pool property change signal only if the pool is + // encrypted. If None, the pool may already be stopped or not exist at all. + // Both of these cases are handled by stop_pool and the value we provide + // for send_locked_signal does not matter as send_locked_signal is only + // used when a pool is newly stopped which can only occur if the pool is found + // here. + let send_locked_signal = block_on(dbus_context.engine.get_pool(LockKey::Uuid(pool_uuid))) + .map(|g| { + let (_, _, p) = g.as_tuple(); + p.is_encrypted() + }) + .unwrap_or(false); + + let msg = match handle_action!(block_on(dbus_context.engine.stop_pool(pool_uuid))) { + Ok(StopAction::Stopped(_)) => { + dbus_context.push_remove(&pool_path, consts::pool_interface_list()); + if send_locked_signal { + dbus_context.push_locked_pools(block_on(dbus_context.engine.locked_pools())); + } + dbus_context.push_stopped_pools(block_on(dbus_context.engine.stopped_pools())); + return_message.append3( + (true, uuid_to_string!(pool_uuid)), + DbusErrorEnum::OK as u16, + OK_STRING.to_string(), + ) + } + Ok(StopAction::Identity) => return_message.append3( + default_return, + DbusErrorEnum::OK as u16, + OK_STRING.to_string(), + ), + Err(e) => { + let (rc, rs) = engine_to_dbus_err_tuple(&e); + return Ok(vec![return_message.append3(default_return, rc, rs)]); + } + }; + + Ok(vec![msg]) +} + +pub fn refresh_state(m: &MethodInfo<'_, MTSync>, TData>) -> MethodResult +where + E: 'static + Engine, +{ + let message: &Message = m.msg; + let dbus_context = m.tree.get_data(); + let return_message = message.method_return(); + + let msg = match block_on(dbus_context.engine.refresh_state()) { + Ok(()) => return_message.append2(DbusErrorEnum::OK as u16, OK_STRING.to_string()), + Err(e) => { + let (rc, rs) = engine_to_dbus_err_tuple(&e); + return Ok(vec![return_message.append2(rc, rs)]); + } + }; + + Ok(vec![msg]) +} diff --git a/src/dbus_api/api/manager_3_2/mod.rs b/src/dbus_api/api/manager_3_2/mod.rs new file mode 100644 index 0000000000..8cb2d1eccc --- /dev/null +++ b/src/dbus_api/api/manager_3_2/mod.rs @@ -0,0 +1,5 @@ +mod api; +mod methods; +mod props; + +pub use api::{refresh_state_method, start_pool_method, stop_pool_method, stopped_pools_property}; diff --git a/src/dbus_api/api/manager_3_2/props.rs b/src/dbus_api/api/manager_3_2/props.rs new file mode 100644 index 0000000000..8d4fcd8bfc --- /dev/null +++ b/src/dbus_api/api/manager_3_2/props.rs @@ -0,0 +1,24 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use dbus::arg::IterAppend; +use dbus_tree::{MTSync, MethodErr, PropInfo}; + +use crate::{ + dbus_api::{ + api::shared::{self, get_manager_property}, + types::TData, + }, + engine::Engine, +}; + +pub fn get_stopped_pools( + i: &mut IterAppend<'_>, + p: &PropInfo<'_, MTSync>, TData>, +) -> Result<(), MethodErr> +where + E: Engine, +{ + get_manager_property(i, p, |e| Ok(shared::stopped_pools_prop(e))) +} diff --git a/src/dbus_api/api/mod.rs b/src/dbus_api/api/mod.rs index 3e486a3bd1..5ef7c51629 100644 --- a/src/dbus_api/api/mod.rs +++ b/src/dbus_api/api/mod.rs @@ -13,6 +13,7 @@ use crate::{ }; mod manager_3_0; +mod manager_3_2; pub mod prop_conv; mod report_3_0; mod shared; @@ -73,11 +74,13 @@ where .add_m(manager_3_0::set_key_method(&f)) .add_m(manager_3_0::unset_key_method(&f)) .add_m(manager_3_0::list_keys_method(&f)) - .add_m(manager_3_0::unlock_pool_method(&f)) .add_m(manager_3_0::destroy_pool_method(&f)) .add_m(manager_3_0::engine_state_report_method(&f)) + .add_m(manager_3_2::start_pool_method(&f)) + .add_m(manager_3_2::stop_pool_method(&f)) + .add_m(manager_3_2::refresh_state_method(&f)) .add_p(manager_3_0::version_property(&f)) - .add_p(manager_3_0::locked_pools_property(&f)), + .add_p(manager_3_2::stopped_pools_property(&f)), ) .add( f.interface(consts::REPORT_INTERFACE_NAME_3_0, ()) diff --git a/src/dbus_api/api/prop_conv.rs b/src/dbus_api/api/prop_conv.rs index 41d657badf..2ed086dbbb 100644 --- a/src/dbus_api/api/prop_conv.rs +++ b/src/dbus_api/api/prop_conv.rs @@ -8,11 +8,11 @@ use dbus::arg::{RefArg, Variant}; use crate::{ dbus_api::util::result_option_to_tuple, - engine::{LockedPoolInfo, PoolUuid}, + engine::{LockedPoolInfo, PoolUuid, StoppedPoolInfo}, }; /// D-Bus representation of locked pools. -pub type LockedPools = HashMap>>>; +pub type StoppedOrLockedPools = HashMap>>>; /// Convert a locked pool data structure to a property format. pub fn locked_pools_to_prop( @@ -69,3 +69,51 @@ pub fn locked_pools_to_prop( }) .collect() } + +/// Convert a stopped pool data structure to a property format. +pub fn stopped_pools_to_prop( + pools: &HashMap, +) -> HashMap>>> { + pools + .iter() + .map(|(u, stopped)| { + let mut map = HashMap::new(); + if let Some(enc_info) = stopped.info.as_ref() { + map.insert( + "key_description".to_string(), + Variant(Box::new(result_option_to_tuple( + enc_info + .key_description() + .map(|opt| opt.map(|kd| kd.as_application_str().to_string())), + String::new(), + )) as Box), + ); + map.insert( + "clevis_info".to_string(), + Variant(Box::new(result_option_to_tuple( + enc_info + .clevis_info() + .map(|opt| opt.map(|(pin, cfg)| (pin.to_owned(), cfg.to_string()))), + (String::new(), String::new()), + )) as Box), + ); + } + map.insert( + "devs".to_string(), + Variant(Box::new( + stopped + .devices + .iter() + .map(|d| { + let mut map = HashMap::new(); + map.insert("devnode".to_string(), d.devnode.display().to_string()); + map.insert("uuid".to_string(), uuid_to_string!(d.uuid)); + map + }) + .collect::>(), + )), + ); + (uuid_to_string!(u), map) + }) + .collect() +} diff --git a/src/dbus_api/api/shared.rs b/src/dbus_api/api/shared.rs index 37b0bd65e1..46ea285a1d 100644 --- a/src/dbus_api/api/shared.rs +++ b/src/dbus_api/api/shared.rs @@ -12,7 +12,7 @@ use futures::executor::block_on; use crate::{ dbus_api::{ - api::prop_conv::{self, LockedPools}, + api::prop_conv::{self, StoppedOrLockedPools}, blockdev::get_blockdev_properties, filesystem::get_fs_properties, pool::get_pool_properties, @@ -184,9 +184,18 @@ where /// Generate D-Bus representation of locked pools #[inline] -pub fn locked_pools_prop(e: &E) -> LockedPools +pub fn locked_pools_prop(e: &E) -> StoppedOrLockedPools where E: Engine, { prop_conv::locked_pools_to_prop(&block_on(e.locked_pools())) } + +/// Generate D-Bus representation of stopped pools +#[inline] +pub fn stopped_pools_prop(e: &E) -> StoppedOrLockedPools +where + E: Engine, +{ + prop_conv::stopped_pools_to_prop(&block_on(e.stopped_pools())) +} diff --git a/src/dbus_api/consts.rs b/src/dbus_api/consts.rs index f2d1535afe..9bc2d50160 100644 --- a/src/dbus_api/consts.rs +++ b/src/dbus_api/consts.rs @@ -15,6 +15,7 @@ pub const REPORT_INTERFACE_NAME_3_1: &str = "org.storage.stratis3.Report.r1"; pub const REPORT_INTERFACE_NAME_3_2: &str = "org.storage.stratis3.Report.r2"; pub const LOCKED_POOLS_PROP: &str = "LockedPools"; +pub const STOPPED_POOLS_PROP: &str = "StoppedPools"; pub const POOL_INTERFACE_NAME_3_0: &str = "org.storage.stratis3.pool.r0"; pub const POOL_INTERFACE_NAME_3_1: &str = "org.storage.stratis3.pool.r1"; diff --git a/src/dbus_api/tree.rs b/src/dbus_api/tree.rs index 32e514bdb1..f8a9cebf4c 100644 --- a/src/dbus_api/tree.rs +++ b/src/dbus_api/tree.rs @@ -24,7 +24,7 @@ use devicemapper::Bytes; use crate::{ dbus_api::{ - api::prop_conv::locked_pools_to_prop, + api::prop_conv::{locked_pools_to_prop, stopped_pools_to_prop}, consts, filesystem::prop_conv::{fs_size_to_prop, fs_used_to_prop}, pool::prop_conv::{ @@ -39,7 +39,7 @@ use crate::{ }, engine::{ ActionAvailability, Engine, FilesystemUuid, LockedPoolInfo, PoolEncryptionInfo, PoolUuid, - StratisUuid, + StoppedPoolInfo, StratisUuid, }, stratis::{StratisError, StratisResult}, }; @@ -378,17 +378,31 @@ where Vec::new(), consts::LOCKED_POOLS_PROP.to_string() => box_variant!(locked_pools_to_prop(&locked_pools)) - }, + } + }, + ) + .is_err() + { + warn!("Signal on locked pools change was not sent to the D-Bus client"); + } + } + + /// Handle a change of stopped pools registered in the engine. + fn handle_stopped_pools_change(&self, stopped_pools: HashMap) { + if self + .property_changed_invalidated_signal( + &Path::new(consts::STRATIS_BASE_PATH).expect("Valid path"), + prop_hashmap! { consts::MANAGER_INTERFACE_NAME_3_2 => { Vec::new(), - consts::LOCKED_POOLS_PROP.to_string() => - box_variant!(locked_pools_to_prop(&locked_pools)) + consts::STOPPED_POOLS_PROP.to_string() => + box_variant!(stopped_pools_to_prop(&stopped_pools)) } }, ) .is_err() { - warn!("Signal on pool available actions mode change was not sent to the D-Bus client"); + warn!("Signal on stopped pools change was not sent to the D-Bus client"); } } @@ -685,6 +699,10 @@ where self.handle_locked_pools_change(pools); Ok(true) } + DbusAction::StoppedPoolsChange(pools) => { + self.handle_stopped_pools_change(pools); + Ok(true) + } DbusAction::PoolForegroundChange(item, new_used, new_alloc, new_size, new_no_space) => { self.handle_pool_foreground_change( item, diff --git a/src/dbus_api/types.rs b/src/dbus_api/types.rs index b1ab65ba50..7d0e10958d 100644 --- a/src/dbus_api/types.rs +++ b/src/dbus_api/types.rs @@ -30,7 +30,8 @@ use crate::{ engine::{ total_allocated, total_used, ActionAvailability, Diff, Engine, ExclusiveGuard, FilesystemUuid, Lockable, LockedPoolInfo, PoolDiff, PoolEncryptionInfo, PoolUuid, - SharedGuard, StratFilesystemDiff, StratPoolDiff, StratisUuid, ThinPoolDiff, + SharedGuard, StoppedPoolInfo, StratFilesystemDiff, StratPoolDiff, StratisUuid, + ThinPoolDiff, }, }; @@ -113,6 +114,7 @@ pub enum DbusAction { PoolFsLimitChange(Path<'static>, u64), PoolOverprovModeChange(Path<'static>, bool), LockedPoolsChange(HashMap), + StoppedPoolsChange(HashMap), FsBackgroundChange( FilesystemUuid, @@ -425,6 +427,19 @@ where } } + /// Send changed signal for changed stopped pool state. + pub fn push_stopped_pools(&self, stopped_pools: HashMap) { + if let Err(e) = self + .sender + .send(DbusAction::StoppedPoolsChange(stopped_pools)) + { + warn!( + "Stopped pool change event could not be sent to the processing thread; no signal will be sent out for the stopped pool state change: {}", + e, + ) + } + } + /// Send changed signal for changed pool properties. pub fn push_pool_foreground_change( &self, diff --git a/src/dbus_api/udev.rs b/src/dbus_api/udev.rs index e2a769cf34..2ce2a951ef 100644 --- a/src/dbus_api/udev.rs +++ b/src/dbus_api/udev.rs @@ -49,7 +49,10 @@ where // is cached, the entire property will be considered changed if any // part changes, and it generally makes more sense to treat the // HashMap comparison as a diff in itself. - let original_state = self.dbus_context.engine.locked_pools().await; + let (original_locked_state, original_stopped_state) = ( + self.dbus_context.engine.locked_pools().await, + self.dbus_context.engine.stopped_pools().await, + ); loop { let recv = self.receiver.recv(); @@ -71,9 +74,13 @@ where self.register_pool(&pool_name, pool_uuid, pool); } - let new_state = self.dbus_context.engine.locked_pools().await; - if original_state != new_state { - self.dbus_context.push_locked_pools(new_state); + let new_locked_state = self.dbus_context.engine.locked_pools().await; + if original_locked_state != new_locked_state { + self.dbus_context.push_locked_pools(new_locked_state); + } + let new_stopped_state = self.dbus_context.engine.stopped_pools().await; + if original_stopped_state != new_stopped_state { + self.dbus_context.push_stopped_pools(new_stopped_state); } Ok(()) diff --git a/src/engine/engine.rs b/src/engine/engine.rs index 096f9d31e1..c803c8c21d 100644 --- a/src/engine/engine.rs +++ b/src/engine/engine.rs @@ -27,7 +27,8 @@ use crate::{ EncryptionInfo, FilesystemUuid, Key, KeyDescription, LockKey, LockedPoolInfo, MappingCreateAction, MappingDeleteAction, Name, PoolDiff, PoolEncryptionInfo, PoolUuid, RegenAction, RenameAction, ReportType, SetCreateAction, SetDeleteAction, - SetUnlockAction, StratFilesystemDiff, UdevEngineEvent, UnlockMethod, + SetUnlockAction, StartAction, StopAction, StoppedPoolInfo, StratFilesystemDiff, + UdevEngineEvent, UnlockMethod, }, }, stratis::StratisResult, @@ -381,6 +382,10 @@ pub trait Engine: Debug + Report + Send + Sync { /// been set up and need to be unlocked to their encryption infos. async fn locked_pools(&self) -> HashMap; + /// Get a mapping of pool UUIDs for pools that have not yet + /// been set up and need to be started to their device infos. + async fn stopped_pools(&self) -> HashMap; + /// Get all pools belonging to this engine. async fn pools(&self) -> AllLockReadGuard; @@ -407,6 +412,21 @@ pub trait Engine: Debug + Report + Send + Sync { /// Get the handler for kernel keyring operations mutably. async fn get_key_handler_mut(&self) -> ExclusiveGuard>; + /// Start and set up a pool, creating all necessary devicemapper devices to + /// perform IO operations and start monitoring for events. + async fn start_pool( + &self, + pool_uuid: PoolUuid, + unlock_method: Option, + ) -> StratisResult>; + + /// Stop and tear down a pool, storing the information for it to be started + /// again later. + async fn stop_pool(&self, pool_uuid: PoolUuid) -> StratisResult>; + + /// Refresh the state of all pools and liminal devices. + async fn refresh_state(&self) -> StratisResult<()>; + /// Return true if this engine is the simulator engine, otherwise false. fn is_sim(&self) -> bool; } diff --git a/src/engine/mod.rs b/src/engine/mod.rs index a03fb6ca54..2d0773ed8c 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -17,8 +17,8 @@ pub use self::{ EncryptionInfo, EngineAction, FilesystemUuid, KeyDescription, LockKey, Lockable, LockedPoolInfo, MappingCreateAction, MappingDeleteAction, MaybeInconsistent, Name, PoolDiff, PoolEncryptionInfo, PoolUuid, RenameAction, ReportType, SetCreateAction, - SetDeleteAction, StratFilesystemDiff, StratPoolDiff, StratisUuid, ThinPoolDiff, - UdevEngineEvent, UnlockMethod, + SetDeleteAction, StartAction, StopAction, StoppedPoolInfo, StratFilesystemDiff, + StratPoolDiff, StratisUuid, ThinPoolDiff, UdevEngineEvent, UnlockMethod, }, }; diff --git a/src/engine/sim_engine/engine.rs b/src/engine/sim_engine/engine.rs index 8da126e33d..f28ef67fff 100644 --- a/src/engine/sim_engine/engine.rs +++ b/src/engine/sim_engine/engine.rs @@ -16,17 +16,18 @@ use tokio::sync::{OwnedRwLockReadGuard, OwnedRwLockWriteGuard, RwLock}; use crate::{ engine::{ - engine::{Engine, Report}, + engine::{Engine, Pool, Report}, shared::{create_pool_idempotent_or_err, validate_name, validate_paths}, sim_engine::{keys::SimKeyActions, pool::SimPool}, structures::{ AllLockReadGuard, AllLockWriteGuard, AllOrSomeLock, ExclusiveGuard, Lockable, - SharedGuard, SomeLockReadGuard, SomeLockWriteGuard, + SharedGuard, SomeLockReadGuard, SomeLockWriteGuard, Table, }, types::{ CreateAction, DeleteAction, DevUuid, EncryptionInfo, FilesystemUuid, LockKey, - LockedPoolInfo, Name, PoolDiff, PoolUuid, RenameAction, ReportType, SetUnlockAction, - StratFilesystemDiff, UdevEngineEvent, UnlockMethod, + LockedPoolInfo, Name, PoolDevice, PoolDiff, PoolUuid, RenameAction, ReportType, + SetUnlockAction, StartAction, StopAction, StoppedPoolInfo, StratFilesystemDiff, + UdevEngineEvent, UnlockMethod, }, }, stratis::{StratisError, StratisResult}, @@ -36,6 +37,7 @@ use crate::{ pub struct SimEngine { pools: AllOrSomeLock, key_handler: Lockable>>, + stopped_pools: Lockable>>>, } impl Default for SimEngine { @@ -43,6 +45,7 @@ impl Default for SimEngine { SimEngine { pools: AllOrSomeLock::default(), key_handler: Lockable::new_shared(SimKeyActions::default()), + stopped_pools: Lockable::new_shared(Table::default()), } } } @@ -68,8 +71,22 @@ impl<'a> Into for &'a SimEngine { }) .collect() ), - "errored_pools": json!([]), - "hopeless_devices": json!([]), + "stopped_pools": Value::Array( + (&*block_on(self.stopped_pools.read())).iter().map(|(name, uuid, pool)| { + let json = json!({ + "pool_uuid": uuid.to_string(), + "name": name.to_string(), + }); + let pool_json = pool.into(); + if let (Value::Object(mut map), Value::Object(submap)) = (json, pool_json) { + map.extend(submap.into_iter()); + Value::Object(map) + } else { + unreachable!("json!() output is always JSON object"); + } + }) + .collect() + ), }) } } @@ -81,10 +98,24 @@ impl Report for SimEngine { fn get_report(&self, report_type: ReportType) -> Value { match report_type { - ReportType::ErroredPoolDevices => json!({ - "errored_pools": json!([]), - "hopeless_devices": json!([]), - }), + ReportType::StoppedPools => { + json!({ + "stopped_pools": (&*block_on(self.stopped_pools.read())).iter().map(|(name, uuid, pool)| { + let json = json!({ + "pool_uuid": uuid.to_string(), + "name": name.to_string(), + }); + let pool_json = pool.into(); + if let (Value::Object(mut map), Value::Object(submap)) = (json, pool_json) { + map.extend(submap.into_iter()); + Value::Object(map) + } else { + unreachable!("json!() output is always JSON object"); + } + }) + .collect::>() + }) + } } } } @@ -210,6 +241,27 @@ impl Engine for SimEngine { HashMap::new() } + async fn stopped_pools(&self) -> HashMap { + let mut map = HashMap::new(); + for (_, uuid, pool) in self.stopped_pools.read().await.iter() { + map.insert( + *uuid, + StoppedPoolInfo { + info: pool.encryption_info(), + devices: pool + .blockdevs() + .into_iter() + .map(|(dev_uuid, _, bd)| PoolDevice { + devnode: bd.devnode().to_path_buf(), + uuid: dev_uuid, + }) + .collect::>(), + }, + ); + } + map + } + async fn pools(&self) -> AllLockReadGuard { self.pools.read_all().await } @@ -241,6 +293,70 @@ impl Engine for SimEngine { self.key_handler.write().await } + async fn start_pool( + &self, + pool_uuid: PoolUuid, + unlock_method: Option, + ) -> StratisResult> { + if let Some(guard) = self.pools.read(LockKey::Uuid(pool_uuid)).await { + let (_, _, pool) = guard.as_tuple(); + if pool.is_encrypted() && unlock_method.is_none() { + return Err(StratisError::Msg(format!( + "Pool with UUID {} is encrypted but no unlock method was provided", + pool_uuid, + ))); + } else if !pool.is_encrypted() && unlock_method.is_some() { + return Err(StratisError::Msg(format!( + "Pool with UUID {} is not encrypted but an unlock method was provided", + pool_uuid, + ))); + } else { + Ok(StartAction::Identity) + } + } else { + let (name, pool) = self + .stopped_pools + .write() + .await + .remove_by_uuid(pool_uuid) + .ok_or_else(|| { + StratisError::Msg(format!( + "Pool with UUID {} was not found and cannot be started", + pool_uuid + )) + })?; + self.pools.write_all().await.insert(name, pool_uuid, pool); + Ok(StartAction::Started(pool_uuid)) + } + } + + async fn stop_pool(&self, pool_uuid: PoolUuid) -> StratisResult> { + if self + .stopped_pools + .read() + .await + .get_by_uuid(pool_uuid) + .is_some() + { + Ok(StopAction::Identity) + } else if let Some((name, pool)) = self.pools.write_all().await.remove_by_uuid(pool_uuid) { + self.stopped_pools + .write() + .await + .insert(name, pool_uuid, pool); + Ok(StopAction::Stopped(pool_uuid)) + } else { + Err(StratisError::Msg(format!( + "Pool with UUID {} was not found and cannot be stopped", + pool_uuid + ))) + } + } + + async fn refresh_state(&self) -> StratisResult<()> { + Ok(()) + } + fn is_sim(&self) -> bool { true } diff --git a/src/engine/strat_engine/backstore/backstore.rs b/src/engine/strat_engine/backstore/backstore.rs index 8b4fb05201..6597402058 100644 --- a/src/engine/strat_engine/backstore/backstore.rs +++ b/src/engine/strat_engine/backstore/backstore.rs @@ -512,19 +512,16 @@ impl Backstore { } /// Teardown the DM devices in the backstore. - #[cfg(test)] pub fn teardown(&mut self) -> StratisResult<()> { match self.cache { - Some(ref mut cache) => cache.teardown(get_dm()), + Some(ref mut cache) => cache.teardown(get_dm())?, None => { if let Some(ref mut linear) = self.linear { - linear.teardown(get_dm()) - } else { - Ok(()) + linear.teardown(get_dm())?; } } - } - .map_err(|e| e.into()) + }; + self.data_tier.block_mgr.teardown() } /// Return the device that this tier is currently using. diff --git a/src/engine/strat_engine/backstore/blockdev.rs b/src/engine/strat_engine/backstore/blockdev.rs index 5d4505490e..cd269e33f9 100644 --- a/src/engine/strat_engine/backstore/blockdev.rs +++ b/src/engine/strat_engine/backstore/blockdev.rs @@ -297,6 +297,20 @@ impl StratBlockDev { })?; crypt_handle.rebind_clevis() } + + /// If a pool is encrypted, tear down the cryptsetup devicemapper devices on the + /// physical device. + pub fn teardown(&mut self) -> StratisResult<()> { + if let Some(ch) = self.underlying_device.crypt_handle() { + debug!( + "Deactivating unlocked encrypted device with UUID {}", + self.bda.dev_uuid() + ); + ch.deactivate() + } else { + Ok(()) + } + } } impl<'a> Into for &'a StratBlockDev { diff --git a/src/engine/strat_engine/backstore/blockdevmgr.rs b/src/engine/strat_engine/backstore/blockdevmgr.rs index bac9d1e57a..cc65e8ab41 100644 --- a/src/engine/strat_engine/backstore/blockdevmgr.rs +++ b/src/engine/strat_engine/backstore/blockdevmgr.rs @@ -690,6 +690,22 @@ impl BlockDevMgr { Ok(()) } } + + /// Tear down devicemapper devices for the block devices in this BlockDevMgr. + pub fn teardown(&mut self) -> StratisResult<()> { + let mut errors = Vec::new(); + for bd in self.block_devs.iter_mut() { + if let Err(e) = bd.teardown() { + errors.push(e); + } + } + + if errors.is_empty() { + Ok(()) + } else { + Err(StratisError::BestEffortError("Failed to remove devicemapper devices for some or all physical devices in the pool".to_string(), errors)) + } + } } fn operation_loop<'a, I, A>(blockdevs: I, action: A) -> StratisResult<()> diff --git a/src/engine/strat_engine/backstore/crypt/consts.rs b/src/engine/strat_engine/backstore/crypt/consts.rs index 5edecee3c3..86333fe516 100644 --- a/src/engine/strat_engine/backstore/crypt/consts.rs +++ b/src/engine/strat_engine/backstore/crypt/consts.rs @@ -25,9 +25,6 @@ pub const STRATIS_MEK_SIZE: usize = 512 / 8; /// Sector size as determined in `cryptsetup/lib/internal.h` pub const SECTOR_SIZE: u64 = 512; -/// Path to logical devices for encrypted devices -pub const DEVICEMAPPER_PATH: &str = "/dev/mapper"; - /// Key in clevis configuration for tang indicating that the URL of the /// tang server does not need to be verified. pub const CLEVIS_TANG_TRUST_URL: &str = "stratis:tang:trust_url"; diff --git a/src/engine/strat_engine/backstore/crypt/handle.rs b/src/engine/strat_engine/backstore/crypt/handle.rs index cd30e2c87d..6ed2c17edb 100644 --- a/src/engine/strat_engine/backstore/crypt/handle.rs +++ b/src/engine/strat_engine/backstore/crypt/handle.rs @@ -2,7 +2,11 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. -use std::{fmt::Debug, path::Path}; +use std::{ + fmt::Debug, + iter::once, + path::{Path, PathBuf}, +}; use either::Either; use serde_json::Value; @@ -23,6 +27,7 @@ use crate::{ }, }, cmd::{clevis_decrypt, clevis_luks_bind, clevis_luks_regen, clevis_luks_unbind}, + dm::DEVICEMAPPER_PATH, keys::MemoryPrivateFilesystem, metadata::StratisIdentifiers, }, @@ -44,35 +49,36 @@ use crate::{ #[derive(Debug, Clone)] pub struct CryptHandle { activated_path: DevicePath, - name: String, metadata_handle: CryptMetadataHandle, } impl CryptHandle { pub(super) fn new( physical_path: DevicePath, - activated_path: DevicePath, identifiers: StratisIdentifiers, encryption_info: EncryptionInfo, name: String, - ) -> CryptHandle { - CryptHandle::new_with_metadata_handle( - activated_path, + ) -> StratisResult { + CryptHandle::new_with_metadata_handle(CryptMetadataHandle::new( + physical_path, + identifiers, + encryption_info, name, - CryptMetadataHandle::new(physical_path, identifiers, encryption_info), - ) + )) } pub(super) fn new_with_metadata_handle( - activated_path: DevicePath, - name: String, metadata_handle: CryptMetadataHandle, - ) -> CryptHandle { - CryptHandle { + ) -> StratisResult { + let activated_path = DevicePath::new( + &once(DEVICEMAPPER_PATH) + .chain(once(metadata_handle.name())) + .collect::(), + )?; + Ok(CryptHandle { activated_path, - name, metadata_handle, - } + }) } /// Acquire the crypt device handle for the physical path in this `CryptHandle`. @@ -80,11 +86,6 @@ impl CryptHandle { acquire_crypt_device(self.luks2_device_path()) } - #[cfg(test)] - pub(super) fn name(&self) -> &str { - &self.name - } - /// Query the device metadata to reconstruct a handle for performing operations /// on an existing encrypted device. /// @@ -125,6 +126,11 @@ impl CryptHandle { &*self.activated_path } + /// Return the name of the activated devicemapper device. + pub fn name(&self) -> &str { + self.metadata_handle.name() + } + /// Get the Stratis device identifiers for a given encrypted device. pub fn device_identifiers(&self) -> &StratisIdentifiers { self.metadata_handle.device_identifiers() @@ -358,9 +364,8 @@ impl CryptHandle { } /// Deactivate the device referenced by the current device handle. - #[cfg(test)] pub fn deactivate(&self) -> StratisResult<()> { - super::shared::ensure_inactive(&mut self.acquire_crypt_device()?, &self.name) + super::shared::ensure_inactive(&mut self.acquire_crypt_device()?, self.name()) } /// Wipe all LUKS2 metadata on the device safely using libcryptsetup. @@ -368,14 +373,14 @@ impl CryptHandle { ensure_wiped( &mut self.acquire_crypt_device()?, self.luks2_device_path(), - &self.name, + self.name(), ) } /// Get the size of the logical device built on the underlying encrypted physical /// device. `devicemapper` will return the size in terms of number of sectors. pub fn logical_device_size(&self) -> StratisResult { - let name = self.name.clone(); + let name = self.name().to_owned(); let active_device = log_on_failure!( self.acquire_crypt_device()? .runtime_handle(&name) diff --git a/src/engine/strat_engine/backstore/crypt/initialize.rs b/src/engine/strat_engine/backstore/crypt/initialize.rs index 605f623c49..caf97e9047 100644 --- a/src/engine/strat_engine/backstore/crypt/initialize.rs +++ b/src/engine/strat_engine/backstore/crypt/initialize.rs @@ -2,10 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. -use std::{ - convert::TryFrom, - path::{Path, PathBuf}, -}; +use std::{convert::TryFrom, path::Path}; use either::Either; use serde_json::Value; @@ -85,35 +82,31 @@ impl CryptInitializer { MetadataSize::try_from(DEFAULT_CRYPT_METADATA_SIZE)?, KeyslotsSize::try_from(DEFAULT_CRYPT_KEYSLOTS_SIZE)?, )?; - let result = self + self .initialize_with_err(&mut device, key_description, clevis_parsed) - .and_then(|path| clevis_info_from_metadata(&mut device).map(|ci| (path, ci))); - - match result { - Ok((activated_path, clevis_info)) => { + .and_then(|path| clevis_info_from_metadata(&mut device).map(|ci| (path, ci))) + .and_then(|(_, clevis_info)| { let encryption_info = EncryptionInfo::from_options((key_description.cloned(), clevis_info)) .expect("Encrypted device must be provided encryption parameters"); - Ok(CryptHandle::new( - self.physical_path, - DevicePath::new(&activated_path)?, + CryptHandle::new( + self.physical_path.clone(), self.identifiers, encryption_info, - self.activation_name, - )) - } - Err(e) => { + self.activation_name.clone(), + ) + }) + .map_err(|e| { if let Err(err) = - Self::rollback(&mut device, &self.physical_path, self.activation_name) + Self::rollback(&mut device, &self.physical_path, &self.activation_name) { warn!( "Failed to roll back crypt device initialization; you may need to manually wipe this device: {}", err ); } - Err(e) - } - } + e + }) } /// Initialize with a passphrase in the kernel keyring only. @@ -213,7 +206,7 @@ impl CryptInitializer { device: &mut CryptDevice, key_description: Option<&KeyDescription>, clevis_info: Option<(&str, &Value, bool)>, - ) -> StratisResult { + ) -> StratisResult<()> { log_on_failure!( device.context_handle().format::<()>( EncryptionFormat::Luks2, @@ -259,8 +252,8 @@ impl CryptInitializer { pub fn rollback( device: &mut CryptDevice, physical_path: &Path, - name: String, + name: &str, ) -> StratisResult<()> { - ensure_wiped(device, physical_path, &name) + ensure_wiped(device, physical_path, name) } } diff --git a/src/engine/strat_engine/backstore/crypt/metadata_handle.rs b/src/engine/strat_engine/backstore/crypt/metadata_handle.rs index 7b7b877960..e379820c10 100644 --- a/src/engine/strat_engine/backstore/crypt/metadata_handle.rs +++ b/src/engine/strat_engine/backstore/crypt/metadata_handle.rs @@ -21,6 +21,7 @@ pub struct CryptMetadataHandle { pub(super) physical_path: DevicePath, pub(super) identifiers: StratisIdentifiers, pub(super) encryption_info: EncryptionInfo, + pub(super) name: String, } impl CryptMetadataHandle { @@ -28,11 +29,13 @@ impl CryptMetadataHandle { physical_path: DevicePath, identifiers: StratisIdentifiers, encryption_info: EncryptionInfo, + name: String, ) -> Self { CryptMetadataHandle { physical_path, identifiers, encryption_info, + name, } } @@ -59,4 +62,9 @@ impl CryptMetadataHandle { pub fn device_identifiers(&self) -> &StratisIdentifiers { &self.identifiers } + + /// Get the name of the activated device when it is activated. + pub fn name(&self) -> &str { + &self.name + } } diff --git a/src/engine/strat_engine/backstore/crypt/shared.rs b/src/engine/strat_engine/backstore/crypt/shared.rs index cf0a8e0358..7b14247623 100644 --- a/src/engine/strat_engine/backstore/crypt/shared.rs +++ b/src/engine/strat_engine/backstore/crypt/shared.rs @@ -5,12 +5,13 @@ use std::{ convert::TryFrom, fs::OpenOptions, - io::{self, Write}, + io::Write, path::{Path, PathBuf}, }; use data_encoding::BASE64URL_NOPAD; use either::Either; +use retry::{delay::Fixed, retry_with_index, Error}; use serde_json::{Map, Value}; use sha2::{Digest, Sha256}; use tempfile::TempDir; @@ -26,10 +27,10 @@ use crate::{ backstore::crypt::{ consts::{ CLEVIS_LUKS_TOKEN_ID, CLEVIS_TANG_TRUST_URL, DEFAULT_CRYPT_KEYSLOTS_SIZE, - DEFAULT_CRYPT_METADATA_SIZE, DEVICEMAPPER_PATH, LUKS2_TOKEN_ID, - LUKS2_TOKEN_TYPE, SECTOR_SIZE, STRATIS_TOKEN_DEVNAME_KEY, - STRATIS_TOKEN_DEV_UUID_KEY, STRATIS_TOKEN_ID, STRATIS_TOKEN_POOL_UUID_KEY, - STRATIS_TOKEN_TYPE, TOKEN_KEYSLOTS_KEY, TOKEN_TYPE_KEY, + DEFAULT_CRYPT_METADATA_SIZE, LUKS2_TOKEN_ID, LUKS2_TOKEN_TYPE, SECTOR_SIZE, + STRATIS_TOKEN_DEVNAME_KEY, STRATIS_TOKEN_DEV_UUID_KEY, STRATIS_TOKEN_ID, + STRATIS_TOKEN_POOL_UUID_KEY, STRATIS_TOKEN_TYPE, TOKEN_KEYSLOTS_KEY, + TOKEN_TYPE_KEY, }, handle::CryptHandle, metadata_handle::CryptMetadataHandle, @@ -226,6 +227,7 @@ pub fn setup_crypt_metadata_handle( physical_path: &Path, ) -> StratisResult> { let identifiers = identifiers_from_metadata(device)?; + let name = name_from_metadata(device)?; let key_description = key_desc_from_metadata(device); let key_description = match key_description .as_ref() @@ -259,6 +261,7 @@ pub fn setup_crypt_metadata_handle( DevicePath::new(physical_path)?, identifiers, encryption_info, + name, ))) } @@ -276,7 +279,7 @@ pub fn setup_crypt_handle( let name = name_from_metadata(device)?; - let activated_path = match unlock_method { + match unlock_method { Some(UnlockMethod::Keyring) => { activate(Either::Left(( device, @@ -290,9 +293,7 @@ pub fn setup_crypt_handle( } Some(UnlockMethod::Clevis) => activate(Either::Right(physical_path), &name)?, None => { - if let Ok(CryptStatusInfo::Active | CryptStatusInfo::Busy) = libcryptsetup_rs::status(Some(device), &name) { - [DEVICEMAPPER_PATH, &name].iter().collect() - } else { + if let Err(_) | Ok(CryptStatusInfo::Inactive | CryptStatusInfo::Invalid) = libcryptsetup_rs::status(Some(device), &name) { return Err(StratisError::Msg( "Found a crypt device but it is not activated and no unlock method was provided".to_string(), )); @@ -300,11 +301,19 @@ pub fn setup_crypt_handle( }, }; - Ok(Some(CryptHandle::new_with_metadata_handle( - DevicePath::new(&activated_path)?, - name, - metadata_handle, - ))) + match CryptHandle::new_with_metadata_handle(metadata_handle) { + Ok(h) => Ok(Some(h)), + Err(e) => { + if let Err(err) = ensure_inactive(device, &name) { + Err(StratisError::NoActionRollbackError { + causal_error: Box::new(e), + rollback_error: Box::new(err), + }) + } else { + Err(e) + } + } + } } /// Create a device handle and load the LUKS2 header into memory from @@ -619,7 +628,7 @@ fn activate_with_keyring(crypt_device: &mut CryptDevice, name: &str) -> StratisR pub fn activate( unlock_param: Either<(&mut CryptDevice, &KeyDescription), &Path>, name: &str, -) -> StratisResult { +) -> StratisResult<()> { let crypt_device = match unlock_param { Either::Left((device, kd)) => { let key_description_missing = keys::search_key_persistent(kd) @@ -652,18 +661,7 @@ pub fn activate( // Check activation status. device_is_active(crypt_device, name)?; - // Checking that the symlink was created may also be valuable in case a race - // condition occurs with udev. - let mut activated_path = PathBuf::from(DEVICEMAPPER_PATH); - activated_path.push(name); - - // Can potentially use inotify with a timeout to wait for the symlink - // if race conditions become a problem. - if activated_path.exists() { - Ok(activated_path) - } else { - Err(StratisError::Io(io::Error::from(io::ErrorKind::NotFound))) - } + Ok(()) } /// Get a list of all keyslots associated with the LUKS2 token. @@ -714,19 +712,39 @@ pub fn get_keyslot_number( /// with devicemapper and cryptsetup. This method is idempotent and leaves /// the state as inactive. pub fn ensure_inactive(device: &mut CryptDevice, name: &str) -> StratisResult<()> { - if log_on_failure!( + let status = log_on_failure!( libcryptsetup_rs::status(Some(device), name), "Failed to determine status of device with name {}", name - ) == CryptStatusInfo::Active - { - log_on_failure!( - device - .activate_handle() - .deactivate(name, CryptDeactivateFlags::empty()), - "Failed to deactivate the crypt device with name {}", - name - ); + ); + match status { + CryptStatusInfo::Active => { + log_on_failure!( + device + .activate_handle() + .deactivate(name, CryptDeactivateFlags::empty()), + "Failed to deactivate the crypt device with name {}", + name + ); + } + CryptStatusInfo::Busy => { + retry_with_index(Fixed::from_millis(100).take(2), |i| { + trace!("Crypt device deactivate attempt {}", i); + device + .activate_handle() + .deactivate(name, CryptDeactivateFlags::empty()) + .map_err(StratisError::Crypt) + }) + .map_err(|e| match e { + Error::Internal(s) => StratisError::Chained( + "Retries for crypt device deactivation failed with an internal error" + .to_string(), + Box::new(StratisError::Msg(s)), + ), + Error::Operation { error, .. } => error, + })?; + } + _ => (), } Ok(()) } diff --git a/src/engine/strat_engine/backstore/devices.rs b/src/engine/strat_engine/backstore/devices.rs index 5138822e4a..6cff7b6f68 100644 --- a/src/engine/strat_engine/backstore/devices.rs +++ b/src/engine/strat_engine/backstore/devices.rs @@ -14,9 +14,10 @@ use std::{ use chrono::Utc; use itertools::Itertools; +use nix::sys::stat::stat; use devicemapper::{Bytes, Device, Sectors, IEC}; -use libblkid_rs::BlkidProbe; +use libblkid_rs::{BlkidCache, BlkidProbe}; use crate::{ engine::{ @@ -31,7 +32,10 @@ use crate::{ BDA, }, names::KeyDescription, - udev::{block_device_apply, decide_ownership, get_udev_property, UdevOwnership}, + udev::{ + block_device_apply, decide_ownership, get_udev_property, UdevOwnership, + STRATIS_FS_TYPE, + }, }, types::{ClevisInfo, DevUuid, DevicePath, EncryptionInfo, PoolUuid}, }, @@ -96,6 +100,50 @@ fn udev_info( }) } +/// Get the device number of a device by stat-ing the device node. +fn get_devno_from_path(path: &Path) -> StratisResult { + let info = stat(path)?; + Ok(Device::from_kdev_t(convert_int!(info.st_rdev, u64, u32)?)) +} + +/// Find all devices that match the given pool and device UUIDs using libblkid. +/// +/// This method is specifically a work around for cases where due to locking +/// internally in stratisd, udev events cannot be used for device identification +/// because they will not have the opportunity to be processed. +pub fn find_stratis_devs_by_uuid( + pool_uuid: PoolUuid, + uuids: Vec, +) -> StratisResult> { + let mut map = HashMap::new(); + if uuids.is_empty() { + return Ok(map); + } + + let mut cache = BlkidCache::get_cache(None)?; + cache.probe_all()?; + for dev in cache.iter().search("TYPE", STRATIS_FS_TYPE)? { + if let Some(dev) = cache.verify(dev) { + let devname = DevicePath::new(&dev.devname()?)?; + let dev_uuid = DevUuid::parse_str(cache.get_tag_value("UUID", &devname)?)?; + let dev_pool_uuid = PoolUuid::parse_str(cache.get_tag_value("POOL_UUID", &devname)?)?; + let devno = get_devno_from_path(&devname)?; + + if dev_pool_uuid == pool_uuid && uuids.contains(&dev_uuid) { + info!( + "Found device with path: {}, pool UUID: {}, device UUID: {} after unlock", + devname.display(), + dev_pool_uuid, + dev_uuid + ); + map.insert(dev_uuid, (DevicePath::new(&devname)?, devno)); + } + } + } + cache.put_cache(); + Ok(map) +} + /// Verify that udev information using a blkid probe to search for superblocks /// and number of partitions on the device. /// diff --git a/src/engine/strat_engine/backstore/mod.rs b/src/engine/strat_engine/backstore/mod.rs index dd4e9d8ef6..07b165bb02 100644 --- a/src/engine/strat_engine/backstore/mod.rs +++ b/src/engine/strat_engine/backstore/mod.rs @@ -21,5 +21,5 @@ pub use self::{ crypt_metadata_size, CryptActivationHandle, CryptHandle, CryptMetadataHandle, CLEVIS_TANG_TRUST_URL, }, - devices::{initialize_devices, process_and_verify_devices}, + devices::{find_stratis_devs_by_uuid, initialize_devices, process_and_verify_devices}, }; diff --git a/src/engine/strat_engine/dm.rs b/src/engine/strat_engine/dm.rs index 1805c912eb..0c7135e627 100644 --- a/src/engine/strat_engine/dm.rs +++ b/src/engine/strat_engine/dm.rs @@ -13,6 +13,9 @@ use crate::stratis::{StratisError, StratisResult}; static INIT: Once = Once::new(); static mut DM_CONTEXT: Option> = None; +/// Path to logical devices for encrypted devices +pub const DEVICEMAPPER_PATH: &str = "/dev/mapper"; + pub fn get_dm_init() -> StratisResult<&'static DM> { unsafe { INIT.call_once(|| DM_CONTEXT = Some(DM::new())); diff --git a/src/engine/strat_engine/engine.rs b/src/engine/strat_engine/engine.rs index dc270205bf..bb396c5a2b 100644 --- a/src/engine/strat_engine/engine.rs +++ b/src/engine/strat_engine/engine.rs @@ -35,10 +35,10 @@ use crate::{ }, types::{ CreateAction, DeleteAction, DevUuid, EncryptionInfo, FilesystemUuid, LockKey, - LockedPoolInfo, PoolDiff, RenameAction, ReportType, SetUnlockAction, - StratFilesystemDiff, UdevEngineEvent, UnlockMethod, + LockedPoolInfo, PoolDiff, RenameAction, ReportType, SetUnlockAction, StartAction, + StopAction, StoppedPoolInfo, StratFilesystemDiff, UdevEngineEvent, UnlockMethod, }, - Engine, Name, PoolUuid, Report, + Engine, Name, Pool, PoolUuid, Report, }, stratis::{StratisError, StratisResult}, }; @@ -291,7 +291,7 @@ impl Report for StratEngine { fn get_report(&self, report_type: ReportType) -> Value { match report_type { - ReportType::ErroredPoolDevices => (&*self.liminal_devices.blocking_read()).into(), + ReportType::StoppedPools => (&*self.liminal_devices.blocking_read()).into(), } } } @@ -313,7 +313,7 @@ impl Engine for StratEngine { events .into_iter() .filter_map(|event| { - if let Some((uuid, name, pool)) = + if let Some((name, uuid, pool)) = ld_guard.block_evaluate(&*pools_write_all, &event) { pools_write_all.insert(name, uuid, pool); @@ -441,7 +441,12 @@ impl Engine for StratEngine { let mut ld_guard = self.liminal_devices.write().await; let unlocked = spawn_blocking!(ld_guard.unlock_pool(&*pools_read_all, pool_uuid, unlock_method,))??; - Ok(SetUnlockAction::new(unlocked)) + Ok(SetUnlockAction::new( + unlocked + .into_iter() + .map(|(uuid, _)| uuid) + .collect::>(), + )) } async fn get_pool( @@ -462,6 +467,10 @@ impl Engine for StratEngine { self.liminal_devices.read().await.locked_pools() } + async fn stopped_pools(&self) -> HashMap { + self.liminal_devices.read().await.stopped_pools() + } + async fn pools(&self) -> AllLockReadGuard { self.pools.read_all().await } @@ -533,6 +542,85 @@ impl Engine for StratEngine { self.key_handler.write().await } + async fn start_pool( + &self, + pool_uuid: PoolUuid, + unlock_method: Option, + ) -> StratisResult> { + if let Some(lock) = self.pools.read(LockKey::Uuid(pool_uuid)).await { + let (_, _, pool) = lock.as_tuple(); + if pool.is_encrypted() && unlock_method.is_none() { + return Err(StratisError::Msg(format!( + "Pool with UUID {} is encrypted but no unlock method was provided", + pool_uuid, + ))); + } else if !pool.is_encrypted() && unlock_method.is_some() { + return Err(StratisError::Msg(format!( + "Pool with UUID {} is not encrypted but an unlock method was provided", + pool_uuid, + ))); + } else { + Ok(StartAction::Identity) + } + } else { + let mut pools = self.pools.write_all().await; + let (name, pool) = + self.liminal_devices + .write() + .await + .start_pool(&*pools, pool_uuid, unlock_method)?; + pools.insert(name, pool_uuid, pool); + Ok(StartAction::Started(pool_uuid)) + } + } + + async fn stop_pool(&self, pool_uuid: PoolUuid) -> StratisResult> { + let mut pools = self.pools.write_all().await; + if let Some((name, mut pool)) = pools.remove_by_uuid(pool_uuid) { + if let Err(e) = self + .liminal_devices + .write() + .await + .stop_pool(&name, pool_uuid, &mut pool) + { + pools.insert(name, pool_uuid, pool); + return Err(e); + } else { + return Ok(StopAction::Stopped(pool_uuid)); + } + } + + drop(pools); + + if self + .liminal_devices + .read() + .await + .stopped_pools() + .get(&pool_uuid) + .is_some() + { + Ok(StopAction::Identity) + } else { + Err(StratisError::Msg(format!( + "Pool with UUID {} could not be found and cannot be stopped", + pool_uuid, + ))) + } + } + + async fn refresh_state(&self) -> StratisResult<()> { + let mut pools = self.pools.write_all().await; + *pools = Table::default(); + let mut lim = self.liminal_devices.write().await; + *lim = LiminalDevices::default(); + let pools_set_up = lim.setup_pools(find_all()?); + for (name, uuid, pool) in pools_set_up { + pools.insert(name, uuid, pool); + } + Ok(()) + } + fn is_sim(&self) -> bool { false } @@ -550,7 +638,7 @@ mod test { backstore::crypt_metadata_size, cmd, ns::unshare_namespace, - tests::{crypt, dm_stratis_devices_remove, loopbacked, real, FailDevice}, + tests::{crypt, loopbacked, real, FailDevice}, }, types::{ActionAvailability, EngineAction, KeyDescription}, }; @@ -726,7 +814,7 @@ mod test { F: Fn(&mut StratPool) -> Result<(), Box>, { fn needs_clean_up( - engine: StratEngine, + engine: &StratEngine, uuid: PoolUuid, fail_device: &FailDevice, operation: F, @@ -756,8 +844,6 @@ mod test { fail_device.stop_failing()?; - engine.teardown()?; - Ok(()) } @@ -772,13 +858,12 @@ mod test { )) })?; - let res = needs_clean_up(engine, uuid, fail_device, operation); + let res = needs_clean_up(&engine, uuid, fail_device, operation); - dm_stratis_devices_remove()?; + test_async!(engine.stop_pool(uuid))?; res?; - let engine = StratEngine::initialize()?; - test_async!(engine.unlock_pool(uuid, unlock_method))?; + test_async!(engine.start_pool(uuid, Some(unlock_method)))?; test_async!(engine.destroy_pool(uuid))?; engine.teardown()?; @@ -1112,10 +1197,49 @@ mod test { } #[test] - fn clevis_real_test_clevis_unbind_rollback() { + fn real_test_clevis_unbind_rollback() { real::test_with_spec( &real::DeviceLimits::AtLeast(2, None, None), test_clevis_unbind_rollback, ); } + + /// Test creating a pool and stopping it. Check that the pool is stopped and + /// then restart the engine, check that it is still stopped, and then start it. + fn test_start_stop(paths: &[&Path]) { + let engine = StratEngine::initialize().unwrap(); + let name = "pool_name"; + let uuid = test_async!(engine.create_pool(name, paths, None)) + .unwrap() + .changed() + .unwrap(); + assert!(test_async!(engine.stop_pool(uuid)).unwrap().is_changed()); + assert_eq!(test_async!(engine.stopped_pools()).len(), 1); + assert_eq!(test_async!(engine.pools()).len(), 0); + + engine.teardown().unwrap(); + + let engine = StratEngine::initialize().unwrap(); + assert_eq!(test_async!(engine.stopped_pools()).len(), 1); + assert_eq!(test_async!(engine.pools()).len(), 0); + + assert!(test_async!(engine.start_pool(uuid, None)) + .unwrap() + .is_changed()); + assert_eq!(test_async!(engine.stopped_pools()).len(), 0); + assert_eq!(test_async!(engine.pools()).len(), 1); + } + + #[test] + fn loop_test_start_stop() { + loopbacked::test_with_spec( + &loopbacked::DeviceLimits::Range(2, 3, None), + test_start_stop, + ); + } + + #[test] + fn real_test_start_stop() { + real::test_with_spec(&real::DeviceLimits::AtLeast(2, None, None), test_start_stop); + } } diff --git a/src/engine/strat_engine/liminal/device_info.rs b/src/engine/strat_engine/liminal/device_info.rs index 382574eae7..afaaa9923b 100644 --- a/src/engine/strat_engine/liminal/device_info.rs +++ b/src/engine/strat_engine/liminal/device_info.rs @@ -7,6 +7,7 @@ use std::{ collections::{hash_map, HashMap, HashSet}, fmt, + iter::FromIterator, path::Path, }; @@ -20,7 +21,8 @@ use crate::{ metadata::StratisIdentifiers, }, types::{ - DevUuid, EncryptionInfo, LockedPoolDevice, LockedPoolInfo, PoolEncryptionInfo, PoolUuid, + DevUuid, EncryptionInfo, LockedPoolInfo, PoolDevice, PoolEncryptionInfo, PoolUuid, + StoppedPoolInfo, }, }, stratis::StratisResult, @@ -248,7 +250,7 @@ impl LInfo { // Combine two devices which have identical pool and device UUIDs. // The first argument is the older information, the second the newer. // Allow the newer information to supplant the older. - fn update(info_1: &LInfo, info_2: &DeviceInfo) -> Result { + fn update(info_1: &LInfo, info_2: &DeviceInfo) -> Option { // Returns true if the information found via udev for two devices is // compatible, otherwise false. // Precondition: Stratis identifiers of devices are the same @@ -266,9 +268,10 @@ impl LInfo { assert_eq!(info_1.ids.identifiers, info_2.identifiers); info_1.ids.device_number == info_2.device_number } + match (info_1, info_2) { (LInfo::Luks(luks_info), DeviceInfo::Stratis(strat_info)) => { - Ok(LInfo::Stratis(LStratisInfo { + Some(LInfo::Stratis(LStratisInfo { ids: strat_info.clone(), luks: Some(luks_info.clone()), })) @@ -276,26 +279,26 @@ impl LInfo { (LInfo::Stratis(strat_info), DeviceInfo::Luks(luks_info)) => { if let Some(luks) = strat_info.luks.as_ref() { if !luks_luks_compatible(luks, luks_info) { - return Err(()); + return None; } } - Ok(LInfo::Stratis(LStratisInfo { + Some(LInfo::Stratis(LStratisInfo { ids: strat_info.ids.clone(), luks: Some(LLuksInfo::from(luks_info.clone())), })) } (LInfo::Luks(luks_info_1), DeviceInfo::Luks(luks_info_2)) => { if !luks_luks_compatible(luks_info_1, luks_info_2) { - Err(()) + None } else { - Ok(LInfo::Luks(LLuksInfo::from(luks_info_2.clone()))) + Some(LInfo::Luks(LLuksInfo::from(luks_info_2.clone()))) } } (LInfo::Stratis(strat_info_1), DeviceInfo::Stratis(strat_info_2)) => { if !stratis_stratis_compatible(strat_info_1, strat_info_2) { - Err(()) + None } else { - Ok(LInfo::Stratis(LStratisInfo { + Some(LInfo::Stratis(LStratisInfo { ids: strat_info_2.clone(), luks: strat_info_1.luks.clone(), })) @@ -320,7 +323,7 @@ impl<'a> Iterator for Iter<'a> { } /// A set of devices, each distinguished by its unique device UUID. -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug, Eq, PartialEq, Clone)] pub struct DeviceSet { internal: HashMap, } @@ -331,6 +334,17 @@ impl Default for DeviceSet { } } +impl FromIterator<(DevUuid, LInfo)> for DeviceSet { + fn from_iter(i: I) -> Self + where + I: IntoIterator, + { + DeviceSet { + internal: HashMap::from_iter(i), + } + } +} + impl DeviceSet { /// Create a new, empty DeviceSet pub fn new() -> DeviceSet { @@ -384,7 +398,7 @@ impl DeviceSet { ) } - /// The encryption information and devices registered for this locked pools to be + /// The encryption information and devices registered for locked pools to be /// exported over the API. If none of the infos correspond to a Stratis managed /// encrypted device, None. /// @@ -414,7 +428,7 @@ impl DeviceSet { } LInfo::Luks(luks_info) => Some(luks_info.ids.devnode.clone()), }; - devnode.map(|devnode| LockedPoolDevice { + devnode.map(|devnode| PoolDevice { devnode, uuid: *uuid, }) @@ -434,6 +448,42 @@ impl DeviceSet { }) } + /// The encryption information and devices registered for stopped pools to + /// be exported over the API. + /// + /// Error from gather_encryption_info is converted into an option because + /// unlocked Stratis devices and LUKS2 devices on which the Stratis devices are + /// stored may appear at different times in udev. This is not necessarily + /// an error case and may resolve itself after more devices appear in udev. + pub fn stopped_pool_info(&self) -> Option { + gather_encryption_info( + self.internal.len(), + self.internal.iter().map(|(_, info)| info.encryption_info()), + ) + .ok() + .map(|info| StoppedPoolInfo { + info, + devices: self + .internal + .iter() + .map(|(uuid, l)| { + let devnode = match l { + LInfo::Stratis(strat_info) => strat_info + .luks + .as_ref() + .map(|l| l.ids.devnode.clone()) + .unwrap_or_else(|| strat_info.ids.devnode.clone()), + LInfo::Luks(luks_info) => luks_info.ids.devnode.clone(), + }; + PoolDevice { + devnode, + uuid: *uuid, + } + }) + .collect::>(), + }) + } + /// Process the data from a remove udev event. Since remove events are /// always subtractive, this method can never introduce a key_description /// which is incompatible with the existing key description. @@ -467,9 +517,9 @@ impl DeviceSet { } /// Process the data from an add udev event. If the added data is - /// incompatible with the existing, drain the set and return all the - /// drained elements, including the incompatible ones. - pub fn process_info_add(&mut self, info: DeviceInfo) -> Result<(), DeviceBag> { + /// incompatible with the existing, warn the user and ignore the new + /// entry. + pub fn process_info_add(&mut self, info: DeviceInfo) { let stratis_identifiers = info.stratis_identifiers(); let device_uuid = stratis_identifiers.device_uuid; @@ -480,22 +530,18 @@ impl DeviceSet { info ); self.internal.insert(device_uuid, info.into()); - Ok(()) } Some(removed) => match LInfo::update(&removed, &info) { - Err(()) => { - let mut hopeless: HashSet = - self.internal.drain().map(|(_, info)| info).collect(); - hopeless.insert(removed); - hopeless.insert(info.into()); - Err(DeviceBag { internal: hopeless }) + None => { + warn!("Found a device information {} that conflicts with an existing registered device; ignoring", info); + self.internal.insert(device_uuid, removed); } - Ok(info) => { + Some(info) => { info!( "Device information {} replaces previous device information for the same device UUID in the set for its pool UUID", - info); + info + ); self.internal.insert(device_uuid, info); - Ok(()) } }, } diff --git a/src/engine/strat_engine/liminal/identify.rs b/src/engine/strat_engine/liminal/identify.rs index 72cec38565..14bd4e9bed 100644 --- a/src/engine/strat_engine/liminal/identify.rs +++ b/src/engine/strat_engine/liminal/identify.rs @@ -51,7 +51,7 @@ use devicemapper::Device; use crate::engine::{ strat_engine::{ - backstore::CryptMetadataHandle, + backstore::{CryptMetadataHandle, StratBlockDev}, metadata::{device_identifiers, StratisIdentifiers}, udev::{ block_enumerator, decide_ownership, UdevOwnership, CRYPTO_FS_TYPE, FS_TYPE_KEY, @@ -148,6 +148,29 @@ impl DeviceInfo { } } +impl From<&StratBlockDev> for DeviceInfo { + fn from(bd: &StratBlockDev) -> Self { + fn stratis_info_from_bd(bd: &StratBlockDev) -> StratisInfo { + StratisInfo { + device_number: *bd.device(), + devnode: bd.physical_path().to_owned(), + identifiers: StratisIdentifiers { + pool_uuid: bd.pool_uuid(), + device_uuid: bd.uuid(), + }, + } + } + + match bd.encryption_info() { + Some(ei) => DeviceInfo::Luks(LuksInfo { + encryption_info: ei.clone(), + info: stratis_info_from_bd(bd), + }), + None => DeviceInfo::Stratis(stratis_info_from_bd(bd)), + } + } +} + impl fmt::Display for DeviceInfo { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -289,6 +312,7 @@ fn find_all_luks_devices() -> libudev::Result>> }); Ok(pool_map) } + // Find all devices identified by udev as Stratis devices. fn find_all_stratis_devices() -> libudev::Result>> { let context = libudev::Context::new()?; diff --git a/src/engine/strat_engine/liminal/liminal.rs b/src/engine/strat_engine/liminal/liminal.rs index f3b8081739..d869b88a5e 100644 --- a/src/engine/strat_engine/liminal/liminal.rs +++ b/src/engine/strat_engine/liminal/liminal.rs @@ -6,79 +6,63 @@ use std::{ collections::{HashMap, HashSet}, - fmt, path::PathBuf, }; +use chrono::{DateTime, Utc}; use serde_json::Value; use crate::{ engine::{ engine::Pool, strat_engine::{ - backstore::CryptActivationHandle, + backstore::{find_stratis_devs_by_uuid, CryptActivationHandle, CryptHandle}, liminal::{ - device_info::{DeviceBag, DeviceSet, LInfo, LLuksInfo}, + device_info::{DeviceSet, LInfo, LLuksInfo, LStratisInfo}, identify::{identify_block_device, DeviceInfo, LuksInfo, StratisInfo}, setup::{get_bdas, get_blockdevs, get_metadata, get_pool_state}, }, - metadata::StratisIdentifiers, + metadata::{StratisIdentifiers, BDA}, pool::StratPool, + serde_structs::PoolSave, }, structures::Table, - types::{DevUuid, LockedPoolInfo, Name, PoolUuid, UdevEngineEvent, UnlockMethod}, + types::{ + DevUuid, LockedPoolInfo, Name, PoolUuid, StoppedPoolInfo, UdevEngineEvent, UnlockMethod, + }, }, stratis::{StratisError, StratisResult}, }; -/// On an error, whether this set of devices is hopeless or just errored -#[derive(Debug)] -enum Destination { - Hopeless(String), - Errored(String), -} - -impl fmt::Display for Destination { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Destination::Hopeless(val) => write!(f, "{}", val), - Destination::Errored(val) => write!(f, "{}", val), - } - } -} - /// Devices which stratisd has discovered but which have not been assembled /// into pools. #[derive(Debug, Default, Eq, PartialEq)] pub struct LiminalDevices { - /// Sets of devices which have not been promoted to pools, but which - /// may still have a chance. - errored_pool_devices: HashMap, - /// Sets of devices which possess some internal contradiction which makes - /// it impossible for them to be made into sensible pools ever. - hopeless_device_sets: HashMap, /// Lookup data structure for pool and device UUIDs corresponding with /// a path where the superblock was either removed or the device was removed. uuid_lookup: HashMap, + /// Devices that have not yet been set up or have been stopped. + stopped_pools: HashMap, } impl LiminalDevices { #[allow(dead_code)] fn invariant(&self) { - assert!(self - .errored_pool_devices - .keys() - .cloned() - .collect::>() - .intersection( - &self - .hopeless_device_sets - .keys() - .cloned() - .collect::>() - ) - .next() - .is_none()); + assert!( + self.stopped_pools + .keys() + .cloned() + .collect::>() + .difference( + &self + .uuid_lookup + .iter() + .map(|(_, (u, _))| *u) + .collect::>() + ) + .count() + == 0 + ); } /// Unlock the liminal encrypted devices that correspond to the given pool UUID. @@ -87,10 +71,13 @@ impl LiminalDevices { pools: &Table, pool_uuid: PoolUuid, unlock_method: UnlockMethod, - ) -> StratisResult> { - fn handle_luks(luks_info: &LLuksInfo, unlock_method: UnlockMethod) -> StratisResult<()> { - if CryptActivationHandle::setup(&luks_info.ids.devnode, unlock_method)?.is_some() { - Ok(()) + ) -> StratisResult> { + fn handle_luks( + luks_info: &LLuksInfo, + unlock_method: UnlockMethod, + ) -> StratisResult { + if let Some(h) = CryptActivationHandle::setup(&luks_info.ids.devnode, unlock_method)? { + Ok(h) } else { Err(StratisError::Msg(format!( "Block device {} does not appear to be formatted with @@ -100,7 +87,7 @@ impl LiminalDevices { } } - let unlocked = match self.errored_pool_devices.get(&pool_uuid) { + let unlocked = match self.stopped_pools.get(&pool_uuid) { Some(map) => { let encryption_info = map.encryption_info(); if let Ok(None) = encryption_info { @@ -125,8 +112,16 @@ impl LiminalDevices { match info { LInfo::Stratis(_) => (), LInfo::Luks(ref luks_info) => match handle_luks(luks_info, unlock_method) { - Ok(()) => unlocked.push(*dev_uuid), - Err(e) => return Err(e), + Ok(handle) => unlocked.push((*dev_uuid, handle)), + Err(e) => { + return Err(handle_unlock_rollback( + e, + unlocked + .into_iter() + .map(|(_, handle)| handle) + .collect::>(), + )); + } }, } } @@ -155,15 +150,116 @@ impl LiminalDevices { Ok(unlocked) } + /// Start a pool, create the devicemapper devices, and return the fully constructed + /// pool. + pub fn start_pool( + &mut self, + pools: &Table, + pool_uuid: PoolUuid, + unlock_method: Option, + ) -> StratisResult<(Name, StratPool)> { + let encryption_info = self + .stopped_pools + .get(&pool_uuid) + .ok_or_else(|| { + StratisError::Msg(format!( + "Requested pool with UUID {} was not found in stopped pools", + pool_uuid + )) + })? + .encryption_info(); + let unlocked_devices = match (encryption_info, unlock_method) { + (Ok(Some(_)), None) => { + return Err(StratisError::Msg(format!( + "Pool with UUID {} is encrypted but no unlock method was provided", + pool_uuid, + ))); + } + (Ok(None), None) => Vec::new(), + (Ok(Some(_)), Some(method)) => self.unlock_pool(pools, pool_uuid, method)?, + (Ok(None), Some(_)) => { + return Err(StratisError::Msg(format!( + "Pool with UUID {} is not encrypted but an unlock method was provided", + pool_uuid, + ))); + } + (Err(e), _) => return Err(e), + }; + + let mut stopped_pool = self + .stopped_pools + .remove(&pool_uuid) + .expect("Checked above"); + match find_stratis_devs_by_uuid( + pool_uuid, + unlocked_devices + .iter() + .map(|(dev_uuid, _)| *dev_uuid) + .collect::>(), + ) { + Ok(infos) => { + for info in infos.into_iter().map(|(dev_uuid, (path, devno))| { + self.uuid_lookup + .insert(path.to_path_buf(), (pool_uuid, dev_uuid)); + DeviceInfo::Stratis(StratisInfo { + device_number: devno, + devnode: path.to_path_buf(), + identifiers: StratisIdentifiers { + pool_uuid, + device_uuid: dev_uuid, + }, + }) + }) { + stopped_pool.process_info_add(info); + } + } + Err(e) => { + warn!("Failed to scan for newly unlocked Stratis devices: {}", e); + } + } + + match self.try_setup_pool(pools, pool_uuid, stopped_pool) { + Ok((name, pool)) => Ok((name, pool)), + Err(e) => Err(handle_unlock_rollback( + e, + unlocked_devices + .into_iter() + .map(|(_, h)| h) + .collect::>(), + )), + } + } + + /// Stop a pool, tear down the devicemapper devices, and store the pool information + /// in an internal data structure for later starting. + pub fn stop_pool( + &mut self, + pool_name: &Name, + pool_uuid: PoolUuid, + pool: &mut StratPool, + ) -> StratisResult<()> { + let devices = pool.stop(pool_name)?; + self.stopped_pools.insert(pool_uuid, devices); + Ok(()) + } + /// Get a mapping of pool UUIDs from all of the LUKS2 devices that are currently /// locked to their encryption info in the set of pools that are not yet set up. pub fn locked_pools(&self) -> HashMap { - self.errored_pool_devices + self.stopped_pools .iter() .filter_map(|(pool_uuid, map)| map.locked_pool_info().map(|info| (*pool_uuid, info))) .collect() } + /// Get a mapping of pool UUIDs to device sets for all stopped pools. + pub fn stopped_pools(&self) -> HashMap { + self.stopped_pools + .iter() + .filter_map(|(pool_uuid, map)| map.stopped_pool_info().map(|info| (*pool_uuid, info))) + .collect() + } + /// Take maps of pool UUIDs to sets of devices and return a list of /// information about created pools. /// @@ -230,155 +326,116 @@ impl LiminalDevices { } let mut info_map = DeviceSet::new(); - while !infos.is_empty() && !self.hopeless_device_sets.contains_key(pool_uuid) { + while !infos.is_empty() { let info: DeviceInfo = infos.pop().expect("!infos.is_empty()"); - if let Err(mut hopeless) = info_map.process_info_add(info) { - hopeless.extend(infos.drain(..).map(|x| x.into())); - self.hopeless_device_sets.insert(*pool_uuid, hopeless); - } + info_map.process_info_add(info); } - if !self.hopeless_device_sets.contains_key(pool_uuid) { - self.try_setup_pool(&table, *pool_uuid, info_map) - .map(|(pool_name, pool)| (pool_name, *pool_uuid, pool)) - } else { - None - } + self.try_setup_started_pool(&table, *pool_uuid, info_map) + .map(|(pool_name, pool)| (pool_name, *pool_uuid, pool)) }) .collect::>() } - /// Given a set of devices, try to set up a pool. - /// Return the pool information if a pool is set up. Otherwise, distribute - /// the pool information to the appropriate data structure. - /// Do not attempt setup if the pool contains any unopened devices. + /// Attempt to set up a pool, starting it if it is not already started. /// - /// If there is a name conflict between the set of devices in devices - /// and some existing pool, return an error. + /// See documentation for setup_pool for more information. /// /// Precondition: pools.get_by_uuid(pool_uuid).is_none() && - /// self.errored_pool_devices.get(pool_uuid).is_none() && - /// self.hopeless_device_sets.get(pool_uuid).is_none() + /// self.stopped_pools.get(pool_uuid).is_none() fn try_setup_pool( &mut self, pools: &Table, pool_uuid: PoolUuid, - infos: DeviceSet, - ) -> Option<(Name, StratPool)> { - assert!(pools.get_by_uuid(pool_uuid).is_none()); - assert!(self.errored_pool_devices.get(&pool_uuid).is_none()); - assert!(self.hopeless_device_sets.get(&pool_uuid).is_none()); - - // Setup a pool from constituent devices in the context of some already - // setup pools. - // - // Precondition: every device represented by an item in infos has - // already been determined to belong to the pool with pool_uuid. - fn setup_pool( + device_set: DeviceSet, + ) -> StratisResult<(Name, StratPool)> { + fn try_setup_pool_failure( pools: &Table, pool_uuid: PoolUuid, device_set: &DeviceSet, - ) -> Result<(Name, StratPool), Destination> { + ) -> StratisResult<(Name, StratPool)> { let infos = match device_set.as_opened_set() { Some(i) => i, None => { - return Err(Destination::Errored(format!( + return Err(StratisError::Msg(format!( "Some of the devices in pool with UUID {} are unopened", pool_uuid, ))) } }; + let (bdas, timestamp, metadata) = load_stratis_metadata(pool_uuid, &infos)?; + setup_pool( + pools, pool_uuid, device_set, &infos, bdas, timestamp, metadata, + ) + } - let bdas = match get_bdas(&infos) { - Err(err) => Err( - Destination::Errored(format!( - "There was an error encountered when reading the BDAs for the devices found for pool with UUID {}: {}", - pool_uuid, - err))), - Ok(infos) => Ok(infos), - }?; - - if let Some((dev_uuid, bda)) = bdas.iter().find(|(dev_uuid, bda)| { - **dev_uuid != bda.dev_uuid() || pool_uuid != bda.pool_uuid() - }) { - return Err( - Destination::Hopeless(format!( - "Mismatch between Stratis identifiers previously read and those found on some BDA: {} != {}", - StratisIdentifiers::new(pool_uuid, *dev_uuid), - StratisIdentifiers::new(bda.pool_uuid(), bda.dev_uuid()) - ))); - } - - let (timestamp, metadata) = match get_metadata(&infos, &bdas) { - Err(err) => return Err( - Destination::Errored(format!( - "There was an error encountered when reading the metadata for the devices found for pool with UUID {}: {}", - pool_uuid, - err))), - Ok(None) => return Err( - Destination::Errored(format!( - "No metadata found on devices associated with pool UUID {}", - pool_uuid))), - Ok(Some((timestamp, metadata))) => (timestamp, metadata), - }; + assert!(pools.get_by_uuid(pool_uuid).is_none()); + assert!(self.stopped_pools.get(&pool_uuid).is_none()); - if let Some((uuid, _)) = pools.get_by_name(&metadata.name) { - return Err( - Destination::Errored(format!( - "There is a pool name conflict. The devices currently being processed have been identified as belonging to the pool with UUID {} and name {}, but a pool with the same name and UUID {} is already active", - pool_uuid, - &metadata.name, - uuid))); + match try_setup_pool_failure(pools, pool_uuid, &device_set) { + Ok((name, pool)) => { + self.uuid_lookup = self + .uuid_lookup + .drain() + .filter(|(_, (p, _))| *p != pool_uuid) + .collect(); + info!( + "Pool with name \"{}\" and UUID \"{}\" set up", + name, pool_uuid + ); + Ok((name, pool)) } - - let (datadevs, cachedevs) = match get_blockdevs(&metadata.backstore, &infos, bdas) { - Err(err) => return Err( - Destination::Errored(format!( - "There was an error encountered when calculating the block devices for pool with UUID {} and name {}: {}", - pool_uuid, - &metadata.name, - err))), - Ok((datadevs, cachedevs)) => (datadevs, cachedevs), - }; - - if datadevs.get(0).is_none() { - return Err(Destination::Hopeless(format!( - "There do not appear to be any data devices in the set with pool UUID {}", - pool_uuid - ))); + Err(err) => { + info!("Attempt to set up pool failed, but it may be possible to set up the pool later, if the situation changes: {}", err); + if !device_set.is_empty() { + self.stopped_pools.insert(pool_uuid, device_set); + } + Err(err) } + } + } - let encryption_info = match device_set.encryption_info() { - Ok(opt) => opt, - Err(_) => { - // NOTE: This is not actually a hopeless situation. It may be - // that a LUKS device owned by Stratis corresponding to a - // Stratis device has just not been discovered yet. If it - // is, the appropriate info will be updated, and setup may - // yet succeed. - return Err( - Destination::Errored(format!( - "Some data devices in the set belonging to pool with UUID {} and name {} appear to be encrypted devices managed by Stratis, and some do not", - pool_uuid, - &metadata.name))); + /// Variation on try_setup_pool that returns None if the pool is marked + /// as stopped in its metadata. + /// + /// Precondition: pools.get_by_uuid(pool_uuid).is_none() && + /// self.stopped_pools.get(pool_uuid).is_none() + fn try_setup_started_pool( + &mut self, + pools: &Table, + pool_uuid: PoolUuid, + device_set: DeviceSet, + ) -> Option<(Name, StratPool)> { + fn try_setup_started_pool_failure( + pools: &Table, + pool_uuid: PoolUuid, + device_set: &DeviceSet, + ) -> StratisResult> { + let infos = match device_set.as_opened_set() { + Some(i) => i, + None => { + return Err(StratisError::Msg(format!( + "Some of the devices in pool with UUID {} are unopened", + pool_uuid, + ))) } }; - - let state = get_pool_state(encryption_info); - StratPool::setup(pool_uuid, datadevs, cachedevs, timestamp, &metadata, state).map_err( - |err| { - Destination::Errored(format!( - "An attempt to set up pool with UUID {} from the assembled devices failed: {}", - pool_uuid, err - )) - }, - ) + let (bdas, timestamp, metadata) = load_stratis_metadata(pool_uuid, &infos)?; + if let Some(true) | None = metadata.started { + setup_pool( + pools, pool_uuid, device_set, &infos, bdas, timestamp, metadata, + ) + .map(Some) + } else { + Ok(None) + } } - let result = setup_pool(pools, pool_uuid, &infos); + assert!(pools.get_by_uuid(pool_uuid).is_none()); + assert!(self.stopped_pools.get(&pool_uuid).is_none()); - match result { - Ok((pool_name, pool)) => { + match try_setup_started_pool_failure(pools, pool_uuid, &device_set) { + Ok(Some((name, pool))) => { self.uuid_lookup = self .uuid_lookup .drain() @@ -386,23 +443,20 @@ impl LiminalDevices { .collect(); info!( "Pool with name \"{}\" and UUID \"{}\" set up", - pool_name, pool_uuid + name, pool_uuid ); - Some((pool_name, pool)) + Some((name, pool)) } - Err(Destination::Hopeless(err)) => { - warn!( - "Attempt to set up pool failed, moving to hopeless devices: {}", - err - ); - self.hopeless_device_sets - .insert(pool_uuid, infos.into_bag()); + Ok(None) => { + if !device_set.is_empty() { + self.stopped_pools.insert(pool_uuid, device_set); + } None } - Err(Destination::Errored(err)) => { + Err(err) => { info!("Attempt to set up pool failed, but it may be possible to set up the pool later, if the situation changes: {}", err); - if !infos.is_empty() { - self.errored_pool_devices.insert(pool_uuid, infos); + if !device_set.is_empty() { + self.stopped_pools.insert(pool_uuid, device_set); } None } @@ -419,18 +473,27 @@ impl LiminalDevices { &mut self, pools: &Table, event: &UdevEngineEvent, - ) -> Option<(PoolUuid, Name, StratPool)> { + ) -> Option<(Name, PoolUuid, StratPool)> { let event_type = event.event_type(); - let device_info = identify_block_device(event); let device_path = match event.device().devnode() { Some(d) => d, None => return None, }; + let device_info = match event_type { + libudev::EventType::Add | libudev::EventType::Change => { + if device_path.exists() { + identify_block_device(event) + } else { + None + } + } + _ => None, + }; if event_type == libudev::EventType::Add || (event_type == libudev::EventType::Change && device_info.is_some()) { - device_info.and_then(move |info| { + if let Some(info) = device_info { let stratis_identifiers = info.stratis_identifiers(); let pool_uuid = stratis_identifiers.pool_uuid; let device_uuid = stratis_identifiers.device_uuid; @@ -443,37 +506,22 @@ impl LiminalDevices { // FIXME: There might be something to check if the device is // included in the pool, but that is less clear. None - } else if let Some(mut set) = self.hopeless_device_sets.remove(&pool_uuid) { - set.insert(info.into()); - self.hopeless_device_sets.insert(pool_uuid, set); - self.uuid_lookup.insert(device_path.to_path_buf(), (pool_uuid, device_uuid)); - None } else { let mut devices = self - .errored_pool_devices + .stopped_pools .remove(&pool_uuid) .unwrap_or_else(DeviceSet::new); - self.uuid_lookup.insert(device_path.to_path_buf(), (pool_uuid, device_uuid)); + self.uuid_lookup + .insert(device_path.to_path_buf(), (pool_uuid, device_uuid)); - if let Err(hopeless) = devices.process_info_add(info) { - self.hopeless_device_sets.insert(pool_uuid, hopeless); - return None; - } - - // FIXME: An attempt to set up the pool is made, even if no - // new device has been added to the set of devices that appear - // to belong to the pool. The reason for this is that there - // may be many causes of failure to set up a pool, and that - // it may be worth another try. If an attempt to setup the - // pool is only made on discovery of a new device that may - // leave a pool that could be set up in limbo forever. An - // alternative, where the user can explicitly ask to try to - // set up an incomplete pool would be a better choice. - self.try_setup_pool(pools, pool_uuid, devices) - .map(|(name, pool)| (pool_uuid, name, pool)) + devices.process_info_add(info); + self.try_setup_started_pool(pools, pool_uuid, devices) + .map(|(name, pool)| (name, pool_uuid, pool)) } - }) + } else { + None + } } else if (event_type == libudev::EventType::Change && device_info.is_none()) || event_type == libudev::EventType::Remove { @@ -491,25 +539,19 @@ impl LiminalDevices { warn!("udev reports that a device with UUID {} that appears to belong to a pool with UUID {} has just been removed; this is likely to result in data loss", dev_uuid, pool_uuid); - None - } else if let Some(bag) = self.hopeless_device_sets.get_mut(&pool_uuid) { - bag.remove(&*device_path, pool_uuid, dev_uuid); - self.uuid_lookup.remove(device_path); - None - } else if self.errored_pool_devices.get(&pool_uuid).is_some() { + } else if self.stopped_pools.get(&pool_uuid).is_some() { let mut devices = self - .errored_pool_devices + .stopped_pools .remove(&pool_uuid) .unwrap_or_else(DeviceSet::new); devices.process_info_remove(device_path, pool_uuid, dev_uuid); self.uuid_lookup.remove(device_path); - - self.try_setup_pool(pools, pool_uuid, devices) - .map(|(name, pool)| (pool_uuid, name, pool)) - } else { - None + if !devices.is_empty() { + self.stopped_pools.insert(pool_uuid, devices); + } } + None } else { None } @@ -519,24 +561,13 @@ impl LiminalDevices { impl<'a> Into for &'a LiminalDevices { fn into(self) -> Value { json!({ - "errored_pools": Value::Array( - self.errored_pool_devices - .iter() - .map(|(uuid, map)| { - json!({ - "pool_uuid": uuid.to_string(), - "devices": <&DeviceSet as Into>::into(map), - }) - }) - .collect(), - ), - "hopeless_devices": Value::Array( - self.hopeless_device_sets + "stopped_pools": Value::Array( + self.stopped_pools .iter() .map(|(uuid, set)| { json!({ "pool_uuid": uuid.to_string(), - "devices": <&DeviceBag as Into>::into(set), + "devices": <&DeviceSet as Into>::into(set), }) }) .collect() @@ -544,3 +575,143 @@ impl<'a> Into for &'a LiminalDevices { }) } } + +/// Read the BDA and MDA information for a set of devices that has been +/// determined to be a part of the same pool. +fn load_stratis_metadata( + pool_uuid: PoolUuid, + infos: &HashMap, +) -> StratisResult<(HashMap, DateTime, PoolSave)> { + let bdas = match get_bdas(infos) { + Err(err) => Err(StratisError::Chained( + format!( + "There was an error encountered when reading the BDAs for the devices found for pool with UUID {}", + pool_uuid, + ), + Box::new(err) + )), + Ok(infos) => Ok(infos), + }?; + + if let Some((dev_uuid, bda)) = bdas + .iter() + .find(|(dev_uuid, bda)| **dev_uuid != bda.dev_uuid() || pool_uuid != bda.pool_uuid()) + { + return Err( + StratisError::Msg(format!( + "Mismatch between Stratis identifiers previously read and those found on some BDA: {} != {}", + StratisIdentifiers::new(pool_uuid, *dev_uuid), + StratisIdentifiers::new(bda.pool_uuid(), bda.dev_uuid()) + ))); + } + + match get_metadata(infos, &bdas) { + Err(err) => Err( + StratisError::Chained( + format!( + "There was an error encountered when reading the metadata for the devices found for pool with UUID {}", + pool_uuid, + ), + Box::new(err) + )), + Ok(None) => Err(StratisError::Msg(format!( + "No metadata found on devices associated with pool UUID {}", + pool_uuid + ))), + Ok(Some((timestamp, metadata))) => Ok((bdas, timestamp, metadata)), + } +} + +/// Given a set of devices, try to set up a pool. +/// Return the pool information if a pool is set up. Otherwise, return +/// the pool information to the stopped pools data structure. +/// Do not attempt setup if the pool contains any unopened devices. +/// +/// If there is a name conflict between the set of devices in devices +/// and some existing pool, return an error. +fn setup_pool( + pools: &Table, + pool_uuid: PoolUuid, + device_set: &DeviceSet, + infos: &HashMap, + bdas: HashMap, + timestamp: DateTime, + metadata: PoolSave, +) -> StratisResult<(Name, StratPool)> { + if let Some((uuid, _)) = pools.get_by_name(&metadata.name) { + return Err( + StratisError::Msg(format!( + "There is a pool name conflict. The devices currently being processed have been identified as belonging to the pool with UUID {} and name {}, but a pool with the same name and UUID {} is already active", + pool_uuid, + &metadata.name, + uuid + ))); + } + + let (datadevs, cachedevs) = match get_blockdevs(&metadata.backstore, infos, bdas) { + Err(err) => return Err( + StratisError::Chained( + format!( + "There was an error encountered when calculating the block devices for pool with UUID {} and name {}", + pool_uuid, + &metadata.name, + ), + Box::new(err) + )), + Ok((datadevs, cachedevs)) => (datadevs, cachedevs), + }; + + if datadevs.get(0).is_none() { + return Err(StratisError::Msg(format!( + "There do not appear to be any data devices in the set with pool UUID {}", + pool_uuid + ))); + } + + let encryption_info = match device_set.encryption_info() { + Ok(opt) => opt, + Err(_) => { + // NOTE: This is not actually a hopeless situation. It may be + // that a LUKS device owned by Stratis corresponding to a + // Stratis device has just not been discovered yet. If it + // is, the appropriate info will be updated, and setup may + // yet succeed. + return Err( + StratisError::Msg(format!( + "Some data devices in the set belonging to pool with UUID {} and name {} appear to be encrypted devices managed by Stratis, and some do not", + pool_uuid, + &metadata.name + ))); + } + }; + + let state = get_pool_state(encryption_info); + StratPool::setup(pool_uuid, datadevs, cachedevs, timestamp, &metadata, state).map_err(|err| { + StratisError::Chained( + format!( + "An attempt to set up pool with UUID {} from the assembled devices failed", + pool_uuid + ), + Box::new(err), + ) + }) +} + +/// Rollback an unlock operation for some or all devices of a pool that have been +/// unlocked prior to the failure occuring. +fn handle_unlock_rollback(causal_error: StratisError, handles: Vec) -> StratisError { + for handle in handles { + if let Err(e) = handle.deactivate() { + warn!("Failed to roll back encrypted pool unlock; some previously locked encrypted devices may be left in an unlocked state"); + return StratisError::NoActionRollbackError { + causal_error: Box::new(causal_error), + rollback_error: Box::new(StratisError::Chained( + "Failed to roll back encrypted pool unlock; some previously locked encrypted devices may be left in an unlocked state".to_string(), + Box::new(e), + )), + }; + } + } + + causal_error +} diff --git a/src/engine/strat_engine/liminal/mod.rs b/src/engine/strat_engine/liminal/mod.rs index 0c7f736896..ef101767f9 100644 --- a/src/engine/strat_engine/liminal/mod.rs +++ b/src/engine/strat_engine/liminal/mod.rs @@ -8,4 +8,8 @@ mod identify; mod liminal; mod setup; -pub use self::{identify::find_all, liminal::LiminalDevices}; +pub use self::{ + device_info::{DeviceSet, LInfo}, + identify::{find_all, DeviceInfo}, + liminal::LiminalDevices, +}; diff --git a/src/engine/strat_engine/pool.rs b/src/engine/strat_engine/pool.rs index e54a7313be..92a8d0ae7b 100644 --- a/src/engine/strat_engine/pool.rs +++ b/src/engine/strat_engine/pool.rs @@ -19,6 +19,7 @@ use crate::{ }, strat_engine::{ backstore::{Backstore, StratBlockDev}, + liminal::{DeviceInfo, DeviceSet, LInfo}, metadata::MDADataSize, serde_structs::{FlexDevsSave, PoolSave, Recordable}, thinpool::{StratFilesystem, ThinPool, ThinPoolSizeParams, DATA_BLOCK_SIZE}, @@ -241,6 +242,9 @@ impl StratPool { metadata_size, }; + // Change the pool to started at this point not that the pool has been set up. + needs_save |= !metadata.started.unwrap_or(false); + if needs_save { pool.write_metadata(pool_name)?; } @@ -309,6 +313,7 @@ impl StratPool { backstore: self.backstore.record(), flex_devs: self.thin_pool.record(), thinpool_dev: self.thin_pool.record(), + started: Some(true), } } @@ -350,6 +355,23 @@ impl StratPool { } } + /// Stop a pool, consuming it and converting it into a set of devices to be + /// set up again later. + pub fn stop(&mut self, pool_name: &Name) -> StratisResult { + self.thin_pool.teardown()?; + let mut data = self.record(pool_name); + data.started = Some(false); + let json = serde_json::to_string(&data)?; + self.backstore.save_state(json.as_bytes())?; + self.backstore.teardown()?; + Ok(self + .backstore + .blockdevs() + .into_iter() + .map(|(uuid, _, bd)| (uuid, LInfo::from(DeviceInfo::from(bd)))) + .collect::()) + } + #[cfg(test)] #[pool_mutating_action("NoRequests")] #[pool_rollback] diff --git a/src/engine/strat_engine/serde_structs.rs b/src/engine/strat_engine/serde_structs.rs index 37d8814c3a..8c5001ec50 100644 --- a/src/engine/strat_engine/serde_structs.rs +++ b/src/engine/strat_engine/serde_structs.rs @@ -37,6 +37,8 @@ pub struct PoolSave { pub backstore: BackstoreSave, pub flex_devs: FlexDevsSave, pub thinpool_dev: ThinPoolDevSave, + #[serde(skip_serializing_if = "Option::is_none")] + pub started: Option, } #[derive(Debug, Deserialize, Eq, PartialEq, Serialize)] diff --git a/src/engine/types/actions.rs b/src/engine/types/actions.rs index 2f14d5dbf6..88845f2832 100644 --- a/src/engine/types/actions.rs +++ b/src/engine/types/actions.rs @@ -615,3 +615,73 @@ impl Display for RegenAction { ) } } + +/// Action indicating an operation for starting a resource +pub enum StartAction { + Identity, + Started(T), +} + +impl Display for StartAction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + StartAction::Identity => write!( + f, + "The requested pool is already started; no action was taken" + ), + StartAction::Started(uuid) => { + write!(f, "The pool with UUID {} was successfully started", uuid) + } + } + } +} + +impl EngineAction for StartAction { + type Return = T; + + fn is_changed(&self) -> bool { + matches!(self, StartAction::Started(_)) + } + + fn changed(self) -> Option { + match self { + StartAction::Started(t) => Some(t), + _ => None, + } + } +} + +/// Action indicating an operation for stopped a resource +pub enum StopAction { + Identity, + Stopped(T), +} + +impl Display for StopAction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + StopAction::Identity => write!( + f, + "The requested pool is already stopped; no action was taken" + ), + StopAction::Stopped(uuid) => { + write!(f, "The pool with UUID {} was successfully stopped", uuid) + } + } + } +} + +impl EngineAction for StopAction { + type Return = T; + + fn is_changed(&self) -> bool { + matches!(self, StopAction::Stopped(_)) + } + + fn changed(self) -> Option { + match self { + StopAction::Stopped(t) => Some(t), + _ => None, + } + } +} diff --git a/src/engine/types/mod.rs b/src/engine/types/mod.rs index 2712797168..924b0e930d 100644 --- a/src/engine/types/mod.rs +++ b/src/engine/types/mod.rs @@ -25,7 +25,7 @@ pub use crate::engine::{ actions::{ Clevis, CreateAction, DeleteAction, EngineAction, Key, MappingCreateAction, MappingDeleteAction, RegenAction, RenameAction, SetCreateAction, SetDeleteAction, - SetUnlockAction, + SetUnlockAction, StartAction, StopAction, }, diff::{Compare, Diff, PoolDiff, StratFilesystemDiff, StratPoolDiff, ThinPoolDiff}, keys::{EncryptionInfo, KeyDescription, PoolEncryptionInfo, SizedKeyMemory}, @@ -203,7 +203,7 @@ impl fmt::Display for Name { /// * `ErroredPoolDevices` returns the state of devices that caused an error while /// attempting to reconstruct a pool. pub enum ReportType { - ErroredPoolDevices, + StoppedPools, } impl<'a> TryFrom<&'a str> for ReportType { @@ -211,7 +211,7 @@ impl<'a> TryFrom<&'a str> for ReportType { fn try_from(name: &str) -> StratisResult { match name { - "errored_pool_report" => Ok(ReportType::ErroredPoolDevices), + "stopped_pools" => Ok(ReportType::StoppedPools), _ => Err(StratisError::Msg(format!( "Report name {} not understood", name @@ -221,7 +221,7 @@ impl<'a> TryFrom<&'a str> for ReportType { } #[derive(Debug, PartialEq)] -pub struct LockedPoolDevice { +pub struct PoolDevice { pub devnode: PathBuf, pub uuid: DevUuid, } @@ -229,7 +229,13 @@ pub struct LockedPoolDevice { #[derive(Debug, PartialEq)] pub struct LockedPoolInfo { pub info: PoolEncryptionInfo, - pub devices: Vec, + pub devices: Vec, +} + +#[derive(Debug, PartialEq)] +pub struct StoppedPoolInfo { + pub info: Option, + pub devices: Vec, } /// A sendable event with all of the necessary information for the engine diff --git a/src/jsonrpc/client/key.rs b/src/jsonrpc/client/key.rs index f9c24856bb..e7a1bdfc76 100644 --- a/src/jsonrpc/client/key.rs +++ b/src/jsonrpc/client/key.rs @@ -20,7 +20,7 @@ pub fn key_set(key_desc: KeyDescription, keyfile_path: Option<&str>) -> StratisR } None => { let password = - rpassword::prompt_password_stdout("Enter passphrase followed by return:")?; + rpassword::prompt_password_stdout("Enter passphrase followed by return: ")?; if password.is_empty() { return Ok(()); } diff --git a/src/jsonrpc/client/pool.rs b/src/jsonrpc/client/pool.rs index 0088a40c38..960fd52efb 100644 --- a/src/jsonrpc/client/pool.rs +++ b/src/jsonrpc/client/pool.rs @@ -22,27 +22,32 @@ pub fn pool_create( do_request_standard!(PoolCreate, name, blockdevs, enc_info) } -// stratis-min pool unlock -pub fn pool_unlock( - unlock_method: UnlockMethod, - uuid: Option, +// stratis-min pool start +pub fn pool_start( + uuid: PoolUuid, + unlock_method: Option, prompt: bool, ) -> StratisResult<()> { if prompt { - let password = rpassword::prompt_password_stdout("Enter passphrase followed by return:")?; + let password = rpassword::prompt_password_stdout("Enter passphrase followed by return: ")?; if password.is_empty() { return Ok(()); } - do_request_standard!(PoolUnlock, unlock_method, uuid; { + do_request_standard!(PoolStart, uuid, unlock_method; { let (read_end, write_end) = pipe()?; write(write_end, password.as_bytes())?; read_end }) } else { - do_request_standard!(PoolUnlock, unlock_method, uuid) + do_request_standard!(PoolStart, uuid, unlock_method) } } +// stratis-min pool stop +pub fn pool_stop(uuid: PoolUuid) -> StratisResult<()> { + do_request_standard!(PoolStop, uuid) +} + // stratis-min pool init-cache pub fn pool_init_cache(name: String, paths: Vec) -> StratisResult<()> { do_request_standard!(PoolInitCache, name, paths) @@ -125,13 +130,13 @@ pub fn pool_is_encrypted(uuid: PoolUuid) -> StratisResult { } } -// stratis-min pool is-locked -pub fn pool_is_locked(uuid: PoolUuid) -> StratisResult { - let (is_locked, rc, rs) = do_request!(PoolIsLocked, uuid); +// stratis-min pool is-stopped +pub fn pool_is_stopped(uuid: PoolUuid) -> StratisResult { + let (is_stopped, rc, rs) = do_request!(PoolIsStopped, uuid); if rc != 0 { Err(StratisError::Msg(rs)) } else { - Ok(is_locked) + Ok(is_stopped) } } diff --git a/src/jsonrpc/interface.rs b/src/jsonrpc/interface.rs index cb302ebd43..f9ffb02d4d 100644 --- a/src/jsonrpc/interface.rs +++ b/src/jsonrpc/interface.rs @@ -39,10 +39,11 @@ pub enum StratisParamType { PoolInitCache(String, Vec), PoolAddCache(String, Vec), PoolDestroy(String), - PoolUnlock(UnlockMethod, Option), + PoolStart(PoolUuid, Option), + PoolStop(PoolUuid), PoolList, PoolIsEncrypted(PoolUuid), - PoolIsLocked(PoolUuid), + PoolIsStopped(PoolUuid), PoolIsBound(PoolUuid), PoolHasPassphrase(PoolUuid), PoolClevisPin(PoolUuid), @@ -71,10 +72,11 @@ pub enum StratisRet { PoolInitCache((bool, u16, String)), PoolAddCache((bool, u16, String)), PoolDestroy((bool, u16, String)), - PoolUnlock((bool, u16, String)), + PoolStart((bool, u16, String)), + PoolStop((bool, u16, String)), PoolList(PoolListType), PoolIsEncrypted((bool, u16, String)), - PoolIsLocked((bool, u16, String)), + PoolIsStopped((bool, u16, String)), PoolIsBound((bool, u16, String)), PoolHasPassphrase((bool, u16, String)), PoolClevisPin((Option, u16, String)), diff --git a/src/jsonrpc/server/pool.rs b/src/jsonrpc/server/pool.rs index c768d72987..a9f41ffcbc 100644 --- a/src/jsonrpc/server/pool.rs +++ b/src/jsonrpc/server/pool.rs @@ -18,39 +18,32 @@ use crate::{ stratis::{StratisError, StratisResult}, }; -// stratis-min pool unlock -pub async fn pool_unlock( +// stratis-min pool start +pub async fn pool_start( engine: Arc, - unlock_method: UnlockMethod, - pool_uuid: Option, + pool_uuid: PoolUuid, + unlock_method: Option, prompt: Option, ) -> StratisResult where E: Engine, { - if let Some(uuid) = pool_uuid { - if let (Some(fd), Some(kd)) = (prompt, key_get_desc(engine.clone(), uuid).await?) { - key_set(engine.clone(), &kd, fd).await?; - } + if let (Some(fd), Some(kd)) = (prompt, key_get_desc(engine.clone(), pool_uuid).await?) { + key_set(engine.clone(), &kd, fd).await?; } - match pool_uuid { - Some(u) => Ok(engine - .unlock_pool(u, unlock_method) - .await? - .changed() - .is_some()), - None => { - let mut changed = false; - for (uuid, _) in engine.locked_pools().await { - let res = engine.unlock_pool(uuid, unlock_method).await; - if let Ok(ok) = res { - changed |= ok.is_changed() - } - } - Ok(changed) - } - } + Ok(engine + .start_pool(pool_uuid, unlock_method) + .await? + .is_changed()) +} + +// stratis-min pool stop +pub async fn pool_stop(engine: Arc, pool_uuid: PoolUuid) -> StratisResult +where + E: Engine, +{ + Ok(engine.stop_pool(pool_uuid).await?.is_changed()) } // stratis-min pool create @@ -213,14 +206,14 @@ where } } -// stratis-min pool is-locked -pub async fn pool_is_locked(engine: Arc, uuid: PoolUuid) -> StratisResult +// stratis-min pool is-stopped +pub async fn pool_is_stopped(engine: Arc, uuid: PoolUuid) -> StratisResult where E: Engine, { if engine.get_pool(LockKey::Uuid(uuid)).await.is_some() { Ok(false) - } else if engine.locked_pools().await.get(&uuid).is_some() { + } else if engine.stopped_pools().await.get(&uuid).is_some() { Ok(true) } else { Err(StratisError::Msg(format!( diff --git a/src/jsonrpc/server/server.rs b/src/jsonrpc/server/server.rs index 1c0979572f..71527fcbb5 100644 --- a/src/jsonrpc/server/server.rs +++ b/src/jsonrpc/server/server.rs @@ -121,9 +121,16 @@ impl StratisParams { false, ))) } - StratisParamType::PoolUnlock(unlock_method, uuid) => { - Ok(StratisRet::PoolUnlock(stratis_result_to_return( - pool::pool_unlock(engine, unlock_method, uuid, self.fd_opt).await, + StratisParamType::PoolStart(uuid, unlock_method) => { + Ok(StratisRet::PoolStart(stratis_result_to_return( + pool::pool_start(engine, uuid, unlock_method, self.fd_opt).await, + false, + ))) + } + StratisParamType::PoolStop(uuid) => { + expects_fd!(self.fd_opt, false); + Ok(StratisRet::PoolStop(stratis_result_to_return( + pool::pool_stop(engine, uuid).await, false, ))) } @@ -138,10 +145,10 @@ impl StratisParams { false, ))) } - StratisParamType::PoolIsLocked(uuid) => { + StratisParamType::PoolIsStopped(uuid) => { expects_fd!(self.fd_opt, false); - Ok(StratisRet::PoolIsLocked(stratis_result_to_return( - pool::pool_is_locked(engine, uuid).await, + Ok(StratisRet::PoolIsStopped(stratis_result_to_return( + pool::pool_is_stopped(engine, uuid).await, false, ))) } diff --git a/systemd/stratis-fstab-setup b/systemd/stratis-fstab-setup index e3a85118b6..1390ed5f58 100755 --- a/systemd/stratis-fstab-setup +++ b/systemd/stratis-fstab-setup @@ -8,7 +8,7 @@ fi POOL_UUID="$1" i=0 -while ! stratis-min pool is-locked "$POOL_UUID" >/dev/null; do +while ! stratis-min pool is-stopped "$POOL_UUID" >/dev/null; do echo Waiting on pool with UUID $POOL_UUID... sleep 1 if [ "$i" = 5 ]; then @@ -17,17 +17,22 @@ while ! stratis-min pool is-locked "$POOL_UUID" >/dev/null; do i=$(($i + 1)) done -if $(stratis-min pool is-locked "$POOL_UUID"); then +if $(stratis-min pool is-stopped "$POOL_UUID"); then if $(stratis-min pool is-bound "$POOL_UUID"); then - if ! stratis-min pool unlock clevis "$POOL_UUID"; then - echo Failed to unlock pool with UUID $POOL_UUID using Clevis. >&2 + if ! stratis-min pool start --unlock-method=clevis "$POOL_UUID"; then + echo Failed to start pool with UUID $POOL_UUID using Clevis. >&2 exit 1 fi - else + elif $(stratis-min pool is-encrypted "$POOL_UUID"); then if ! systemd-ask-password \ "Enter password for pool with UUID $POOL_UUID" | - stratis-min pool unlock keyring --prompt "$POOL_UUID"; then - echo Failed to unlock pool with UUID $POOL_UUID. >&2 + stratis-min pool start --unlock-method=keyring --prompt "$POOL_UUID"; then + echo Failed to start pool with UUID $POOL_UUID using passphrase. >&2 + exit 1 + fi + else + if ! stratis-min pool start "$POOL_UUID"; then + echo Failed to start pool with UUID $POOL_UUID. >&2 exit 1 fi fi diff --git a/tests/client-dbus/src/stratisd_client_dbus/_introspect.py b/tests/client-dbus/src/stratisd_client_dbus/_introspect.py index 8262380da8..4e0c0b8f30 100644 --- a/tests/client-dbus/src/stratisd_client_dbus/_introspect.py +++ b/tests/client-dbus/src/stratisd_client_dbus/_introspect.py @@ -41,10 +41,16 @@ - + - - + + + + + + + + @@ -54,7 +60,7 @@ - + diff --git a/tests/client-dbus/tests/udev/test_udev.py b/tests/client-dbus/tests/udev/test_udev.py index 224f50b129..e88da2800a 100644 --- a/tests/client-dbus/tests/udev/test_udev.py +++ b/tests/client-dbus/tests/udev/test_udev.py @@ -264,20 +264,19 @@ def _simple_initial_discovery_test( remove_stratis_dm_devices() with OptionalKeyServiceContextManager(key_spec=key_spec): - ((option, unlock_uuids), exit_code, _) = Manager.Methods.UnlockPool( + ((changed, _), exit_code, _) = Manager.Methods.StartPool( get_object(TOP_OBJECT), { "pool_uuid": pool_uuid, - "unlock_method": str(EncryptionMethod.KEYRING), + "unlock_method": (True, str(EncryptionMethod.KEYRING)), }, ) if key_spec is None: self.assertNotEqual(exit_code, StratisdErrors.OK) - self.assertEqual(option, False) + self.assertEqual(changed, False) else: self.assertEqual(exit_code, StratisdErrors.OK) - self.assertEqual(option, take_down_dm) - self.assertEqual(len(unlock_uuids), num_devices if take_down_dm else 0) + self.assertEqual(changed, take_down_dm) wait_for_udev_count(num_devices) @@ -367,20 +366,17 @@ def _simple_event_test(self, *, key_spec=None): # pylint: disable=too-many-loca wait_for_udev(udev_wait_type, self._lb_mgr.device_files(tokens_up)) self.wait_for_pools(0) - ((option, unlock_uuids), exit_code, _) = Manager.Methods.UnlockPool( + ((changed, _), exit_code, _) = Manager.Methods.StartPool( get_object(TOP_OBJECT), { "pool_uuid": pool_uuid, - "unlock_method": str(EncryptionMethod.KEYRING), + "unlock_method": (True, str(EncryptionMethod.KEYRING)), }, ) - if key_spec is None: - self.assertNotEqual(exit_code, StratisdErrors.OK) - self.assertEqual(option, False) - else: - self.assertEqual(exit_code, StratisdErrors.OK) - self.assertEqual(option, True) - self.assertEqual(len(unlock_uuids), num_devices - 1) + # This should always fail because a pool cannot be successfully + # started without all devices present. + self.assertNotEqual(exit_code, StratisdErrors.OK) + self.assertEqual(changed, False) self.wait_for_pools(0) @@ -389,21 +385,20 @@ def _simple_event_test(self, *, key_spec=None): # pylint: disable=too-many-loca wait_for_udev(udev_wait_type, self._lb_mgr.device_files(tokens_up)) - ((option, unlock_uuids), exit_code, _) = Manager.Methods.UnlockPool( + ((changed, _), exit_code, _) = Manager.Methods.StartPool( get_object(TOP_OBJECT), { "pool_uuid": pool_uuid, - "unlock_method": str(EncryptionMethod.KEYRING), + "unlock_method": (True, str(EncryptionMethod.KEYRING)), }, ) if key_spec is None: self.assertNotEqual(exit_code, StratisdErrors.OK) - self.assertEqual(option, False) + self.assertEqual(changed, False) else: self.assertEqual(exit_code, StratisdErrors.OK) - self.assertEqual(option, True) - self.assertEqual(len(unlock_uuids), 1) + self.assertEqual(changed, True) wait_for_udev_count(num_devices) @@ -442,7 +437,9 @@ class UdevTest5(UdevTest): so forth. Eventually, all pools should have been set up. """ - def test_duplicate_pool_name(self): # pylint: disable=too-many-locals + def test_duplicate_pool_name( + self, + ): # pylint: disable=too-many-locals, too-many-statements """ Create more than one pool with the same name, then dynamically fix it :return: None @@ -505,22 +502,27 @@ def test_duplicate_pool_name(self): # pylint: disable=too-many-locals wait_for_udev(CRYPTO_LUKS_FS_TYPE, self._lb_mgr.device_files(luks_tokens)) wait_for_udev(STRATIS_FS_TYPE, self._lb_mgr.device_files(non_luks_tokens)) - variant_pool_uuids = Manager.Properties.LockedPools.Get( + variant_pool_uuids = Manager.Properties.StoppedPools.Get( get_object(TOP_OBJECT) ) + blockdevs = [] for pool_uuid in variant_pool_uuids: - ((option, _), exit_code, _) = Manager.Methods.UnlockPool( + ( + (changed, (_, blockdevs_tmp, _)), + exit_code, + _, + ) = Manager.Methods.StartPool( get_object(TOP_OBJECT), { "pool_uuid": pool_uuid, - "unlock_method": str(EncryptionMethod.KEYRING), + "unlock_method": (True, str(EncryptionMethod.KEYRING)), }, ) - self.assertEqual(exit_code, StratisdErrors.OK) - self.assertEqual(option, True) + if exit_code == StratisdErrors.OK and changed: + blockdevs = blockdevs_tmp - wait_for_udev_count(len(all_tokens)) + wait_for_udev_count(len(blockdevs) + len(non_luks_tokens)) # The number of pools should never exceed one, since all the pools # previously formed in the test have the same name. @@ -530,6 +532,9 @@ def test_duplicate_pool_name(self): # pylint: disable=too-many-locals # then generate synthetic add events for every loopbacked device. # After num_pools - 1 iterations, all pools should have been set up. for pool_count in range(1, num_pools): + variant_pool_uuids = Manager.Properties.StoppedPools.Get( + get_object(TOP_OBJECT) + ) current_pools = self.wait_for_pools(pool_count) # Rename all active pools to a randomly selected new name @@ -538,7 +543,18 @@ def test_duplicate_pool_name(self): # pylint: disable=too-many-locals get_object(object_path), {"name": random_string(10)} ) - self._lb_mgr.generate_synthetic_udev_events(all_tokens, UDEV_ADD_EVENT) + self._lb_mgr.generate_synthetic_udev_events( + non_luks_tokens, UDEV_ADD_EVENT + ) + for pool_uuid, props in variant_pool_uuids.items(): + if "key_description" in props: + ((changed, _), exit_code, _) = Manager.Methods.StartPool( + get_object(TOP_OBJECT), + { + "pool_uuid": pool_uuid, + "unlock_method": (True, str(EncryptionMethod.KEYRING)), + }, + ) settle() @@ -547,3 +563,87 @@ def test_duplicate_pool_name(self): # pylint: disable=too-many-locals self.wait_for_pools(num_pools) remove_stratis_dm_devices() + + +class UdevTest6(UdevTest): + """ + A test that verifies that unencrypted pools are not set up after having been + stopped. + + Two pools are created, Then the daemon is brought down and all Stratis + devicemapper devices are destroyed. The daemon is brought back up again, and it + is verified that the daemon has recreated the pool. + + One pool is then stopped and then devicemapper devices are removed for the other. + The daemon is then stopped and started and only one pool should be set up. + """ + + def _simple_stop_test(self): + """ + A simple test of stopping pools. + + * Create two unencrypted pools + * Stop the daemon + * Remove devicemapper devices + * Start the daemon + * Ensure two pools are up + * Stop one pool and then the daemon + * Remove devicemapper devices + * Start the daemon + * Ensure only one pool is set up + * Ensure udev events do not cause pool to be set up + """ + num_devices = 2 + device_tokens = self._lb_mgr.create_devices(num_devices) + devnodes = self._lb_mgr.device_files(device_tokens) + + with OptionalKeyServiceContextManager(): + self.wait_for_pools(0) + + (_, (pool_object_path, _)) = create_pool(random_string(5), devnodes[:1]) + create_pool(random_string(5), devnodes[1:]) + + self.wait_for_pools(2) + + remove_stratis_dm_devices() + + with OptionalKeyServiceContextManager(): + self.wait_for_pools(2) + self.assertEqual( + len(Manager.Properties.StoppedPools.Get(get_object(TOP_OBJECT))), + 0, + ) + + ((changed, _), exit_code, _) = Manager.Methods.StopPool( + get_object(TOP_OBJECT), + {"pool": pool_object_path}, + ) + self.assertEqual(exit_code, 0) + self.assertEqual(changed, True) + + self.assertEqual( + len(Manager.Properties.StoppedPools.Get(get_object(TOP_OBJECT))), + 1, + ) + + remove_stratis_dm_devices() + + with OptionalKeyServiceContextManager(): + self.wait_for_pools(1) + self.assertEqual( + len(Manager.Properties.StoppedPools.Get(get_object(TOP_OBJECT))), + 1, + ) + self._lb_mgr.generate_synthetic_udev_events( + device_tokens[:1], UDEV_ADD_EVENT + ) + self.assertEqual( + len(Manager.Properties.StoppedPools.Get(get_object(TOP_OBJECT))), + 1, + ) + + def test_simple_stop(self): + """ + See documentation for _simple_stop_test. + """ + self._simple_stop_test()