From a6414916bb443efd6b68ef17ba7dbc2bf0bdf93f Mon Sep 17 00:00:00 2001 From: John Baublitz Date: Mon, 7 Mar 2022 14:33:53 -0500 Subject: [PATCH] Add the ability to start and stop pools This commit replaces the previous approach of unlocking encrypted pools with full support for starting and stopping pools. Locked encrypted pools are now simply considered stopped. The behavior of stopping pools is as follows. A pool that is started when the system shuts down will be started after booting the system again. A pool that is stopped will remain stopped and will not be set up until it is started again. This operation requires a change to the metadata schema to store whether a pool is stopped or started. Migration from older versions of stratisd is handled. Stopping an encrypted pool will lock all of the encrypted devices. --- .../stratis-clevis-rootfs-setup | 8 +- dracut/90stratis/stratis-rootfs-setup | 23 +- src/bin/stratis-min/stratis-min.rs | 36 +- src/dbus_api/api/manager_3_0/api.rs | 4 +- src/dbus_api/api/manager_3_2/api.rs | 82 +++ src/dbus_api/api/manager_3_2/methods.rs | 208 ++++++ src/dbus_api/api/manager_3_2/mod.rs | 5 + src/dbus_api/api/manager_3_2/props.rs | 24 + src/dbus_api/api/mod.rs | 7 +- src/dbus_api/api/prop_conv.rs | 52 +- src/dbus_api/api/shared.rs | 13 +- src/dbus_api/consts.rs | 1 + src/dbus_api/tree.rs | 29 +- src/dbus_api/types.rs | 17 +- src/dbus_api/udev.rs | 15 +- src/engine/engine.rs | 22 +- src/engine/mod.rs | 4 +- src/engine/sim_engine/engine.rs | 136 +++- .../strat_engine/backstore/backstore.rs | 11 +- src/engine/strat_engine/backstore/blockdev.rs | 14 + .../strat_engine/backstore/blockdevmgr.rs | 16 + .../strat_engine/backstore/crypt/consts.rs | 3 - .../strat_engine/backstore/crypt/handle.rs | 51 +- .../backstore/crypt/initialize.rs | 39 +- .../backstore/crypt/metadata_handle.rs | 8 + .../strat_engine/backstore/crypt/shared.rs | 92 +-- src/engine/strat_engine/backstore/devices.rs | 52 +- src/engine/strat_engine/backstore/mod.rs | 2 +- src/engine/strat_engine/dm.rs | 3 + src/engine/strat_engine/engine.rs | 154 ++++- .../strat_engine/liminal/device_info.rs | 96 ++- src/engine/strat_engine/liminal/identify.rs | 26 +- src/engine/strat_engine/liminal/liminal.rs | 627 +++++++++++------- src/engine/strat_engine/liminal/mod.rs | 6 +- src/engine/strat_engine/pool.rs | 22 + src/engine/strat_engine/serde_structs.rs | 2 + src/engine/types/actions.rs | 70 ++ src/engine/types/mod.rs | 16 +- src/jsonrpc/client/key.rs | 2 +- src/jsonrpc/client/pool.rs | 27 +- src/jsonrpc/interface.rs | 10 +- src/jsonrpc/server/pool.rs | 49 +- src/jsonrpc/server/server.rs | 19 +- systemd/stratis-fstab-setup | 19 +- .../src/stratisd_client_dbus/_introspect.py | 14 +- tests/client-dbus/tests/udev/test_udev.py | 154 ++++- 46 files changed, 1776 insertions(+), 514 deletions(-) create mode 100644 src/dbus_api/api/manager_3_2/api.rs create mode 100644 src/dbus_api/api/manager_3_2/methods.rs create mode 100644 src/dbus_api/api/manager_3_2/mod.rs create mode 100644 src/dbus_api/api/manager_3_2/props.rs diff --git a/dracut/90stratis-clevis/stratis-clevis-rootfs-setup b/dracut/90stratis-clevis/stratis-clevis-rootfs-setup index 846b0152a2e..ae06c46b0b0 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 3eb8da68a02..e14509dd6e6 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 ac43d84aed9..ed82ae2aded 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 5be7f9d4d69..c030fe4bd2f 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 00000000000..d2cd7f3eacc --- /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 00000000000..86465b1d4e2 --- /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 00000000000..8cb2d1eccc0 --- /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 00000000000..8d4fcd8bfcc --- /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 3e486a3bd14..5ef7c516295 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 41d657badfb..2ed086dbbba 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 37b0bd65e14..46ea285a1d0 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 f2d1535afe8..9bc2d50160c 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 32e514bdb1f..b2574f31d33 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}, }; @@ -388,7 +388,26 @@ where ) .is_err() { - warn!("Signal on pool available actions mode change was not sent to the D-Bus client"); + 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::STOPPED_POOLS_PROP.to_string() => + box_variant!(stopped_pools_to_prop(&stopped_pools)) + } + }, + ) + .is_err() + { + warn!("Signal on stopped pools change was not sent to the D-Bus client"); } } @@ -685,6 +704,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 b1ab65ba50c..7d0e10958dd 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 e2a769cf34f..2ce2a951ef8 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 096f9d31e17..c803c8c21df 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 a03fb6ca54e..2d0773ed8cc 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 8da126e33d7..f28ef67fff4 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 8b4fb052011..6597402058e 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 5d4505490e7..cd269e33f97 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 bac9d1e57a6..cc65e8ab41c 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 5edecee3c3e..86333fe5164 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 cd30e2c87d4..6ed2c17edbf 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 605f623c498..caf97e90472 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 7b7b8779604..e379820c109 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 cf0a8e03581..7b142476239 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 5138822e4a2..6cff7b6f689 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 dd4e9d8ef65..07b165bb022 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 1805c912eb8..0c7135e6276 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 fef9be8ddaa..a0ae92d7e0b 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}, udev::{CRYPTO_FS_TYPE, FS_TYPE_KEY}, }, types::{ @@ -729,7 +817,7 @@ mod test { F: Fn(&mut StratPool) -> Result<(), Box>, { fn needs_clean_up( - engine: StratEngine, + engine: &StratEngine, uuid: PoolUuid, fail_device: &FailDevice, operation: F, @@ -759,8 +847,6 @@ mod test { fail_device.stop_failing()?; - engine.teardown()?; - Ok(()) } @@ -809,13 +895,12 @@ mod test { test_async!(engine.handle_events(events)); - 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()?; @@ -1149,10 +1234,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 382574eae79..afaaa9923be 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 72cec385656..14bd4e9bed9 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 f3b80817397..d869b88a5e7 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 0c7f736896c..ef101767f91 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 e54a7313bee..92a8d0ae7b6 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 37d8814c3a3..8c5001ec50c 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 2f14d5dbf6e..88845f2832b 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 2712797168d..924b0e930d0 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 f9c24856bbf..e7a1bdfc76c 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 0088a40c386..960fd52efbf 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 cb302ebd439..f9ffb02d4de 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 c768d72987e..a9f41ffcbca 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 1c0979572f2..71527fcbb5e 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 e3a85118b6f..1390ed5f585 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 8262380da8a..4e0c0b8f309 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 224f50b1293..e88da2800a4 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()