diff --git a/src/dbus_api/consts.rs b/src/dbus_api/consts.rs index 7c9d144172..76bee0b704 100644 --- a/src/dbus_api/consts.rs +++ b/src/dbus_api/consts.rs @@ -60,6 +60,7 @@ pub const FILESYSTEM_DEVNODE_PROP: &str = "Devnode"; pub const FILESYSTEM_POOL_PROP: &str = "Pool"; pub const FILESYSTEM_CREATED_PROP: &str = "Created"; pub const FILESYSTEM_SIZE_PROP: &str = "Size"; +pub const FILESYSTEM_SIZE_LIMIT_PROP: &str = "SizeLimit"; pub const BLOCKDEV_INTERFACE_NAME_3_0: &str = "org.storage.stratis3.blockdev.r0"; pub const BLOCKDEV_INTERFACE_NAME_3_1: &str = "org.storage.stratis3.blockdev.r1"; diff --git a/src/dbus_api/filesystem/filesystem_3_0/props.rs b/src/dbus_api/filesystem/filesystem_3_0/props.rs index cdd1e395bc..22fd4203bf 100644 --- a/src/dbus_api/filesystem/filesystem_3_0/props.rs +++ b/src/dbus_api/filesystem/filesystem_3_0/props.rs @@ -5,39 +5,14 @@ use dbus::arg::IterAppend; use dbus_tree::{MTSync, MethodErr, PropInfo}; -use crate::{ - dbus_api::{ - filesystem::shared::{self, filesystem_operation}, - types::TData, - }, - engine::{Filesystem, Name}, -}; - -/// Get a filesystem property and place it on the D-Bus. The property is -/// found by means of the getter method which takes a reference to a -/// Filesystem and obtains the property from the filesystem. -fn get_filesystem_property( - i: &mut IterAppend<'_>, - p: &PropInfo<'_, MTSync, TData>, - getter: F, -) -> Result<(), MethodErr> -where - F: Fn((Name, Name, &dyn Filesystem)) -> Result, - R: dbus::arg::Append, -{ - i.append( - filesystem_operation(p.tree, p.path.get_name(), getter) - .map_err(|ref e| MethodErr::failed(e))?, - ); - Ok(()) -} +use crate::dbus_api::{filesystem::shared, types::TData}; /// Get the devnode for an object path. pub fn get_filesystem_devnode( i: &mut IterAppend<'_>, p: &PropInfo<'_, MTSync, TData>, ) -> Result<(), MethodErr> { - get_filesystem_property(i, p, |(pool_name, fs_name, fs)| { + shared::get_filesystem_property(i, p, |(pool_name, fs_name, fs)| { Ok(shared::fs_devnode_prop(fs, &pool_name, &fs_name)) }) } @@ -46,7 +21,7 @@ pub fn get_filesystem_name( i: &mut IterAppend<'_>, p: &PropInfo<'_, MTSync, TData>, ) -> Result<(), MethodErr> { - get_filesystem_property(i, p, |(_, fs_name, _)| Ok(shared::fs_name_prop(&fs_name))) + shared::get_filesystem_property(i, p, |(_, fs_name, _)| Ok(shared::fs_name_prop(&fs_name))) } /// Get the creation date and time in rfc3339 format. @@ -54,7 +29,7 @@ pub fn get_filesystem_created( i: &mut IterAppend<'_>, p: &PropInfo<'_, MTSync, TData>, ) -> Result<(), MethodErr> { - get_filesystem_property(i, p, |(_, _, fs)| Ok(shared::fs_created_prop(fs))) + shared::get_filesystem_property(i, p, |(_, _, fs)| Ok(shared::fs_created_prop(fs))) } /// Get the size of the filesystem in bytes. @@ -62,7 +37,7 @@ pub fn get_filesystem_size( i: &mut IterAppend<'_>, p: &PropInfo<'_, MTSync, TData>, ) -> Result<(), MethodErr> { - get_filesystem_property(i, p, |(_, _, fs)| Ok(shared::fs_size_prop(fs))) + shared::get_filesystem_property(i, p, |(_, _, fs)| Ok(shared::fs_size_prop(fs))) } /// Get the size of the used portion of the filesystem in bytes. @@ -70,5 +45,5 @@ pub fn get_filesystem_used( i: &mut IterAppend<'_>, p: &PropInfo<'_, MTSync, TData>, ) -> Result<(), MethodErr> { - get_filesystem_property(i, p, |(_, _, fs)| Ok(shared::fs_used_prop(fs))) + shared::get_filesystem_property(i, p, |(_, _, fs)| Ok(shared::fs_used_prop(fs))) } diff --git a/src/dbus_api/filesystem/filesystem_3_6/api.rs b/src/dbus_api/filesystem/filesystem_3_6/api.rs new file mode 100644 index 0000000000..2334730c0e --- /dev/null +++ b/src/dbus_api/filesystem/filesystem_3_6/api.rs @@ -0,0 +1,20 @@ +// 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, Property}; + +use crate::dbus_api::{ + consts, + filesystem::filesystem_3_6::props::{get_fs_size_limit, set_fs_size_limit}, + types::TData, +}; + +pub fn size_limit_property(f: &Factory, TData>) -> Property, TData> { + f.property::<(bool, String), _>(consts::FILESYSTEM_SIZE_LIMIT_PROP, ()) + .access(Access::ReadWrite) + .emits_changed(EmitsChangedSignal::True) + .auto_emit_on_set(false) + .on_get(get_fs_size_limit) + .on_set(set_fs_size_limit) +} diff --git a/src/dbus_api/filesystem/filesystem_3_6/mod.rs b/src/dbus_api/filesystem/filesystem_3_6/mod.rs new file mode 100644 index 0000000000..322dccc1d5 --- /dev/null +++ b/src/dbus_api/filesystem/filesystem_3_6/mod.rs @@ -0,0 +1,8 @@ +// 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/. + +mod api; +mod props; + +pub use api::size_limit_property; diff --git a/src/dbus_api/filesystem/filesystem_3_6/props.rs b/src/dbus_api/filesystem/filesystem_3_6/props.rs new file mode 100644 index 0000000000..f0fc4eb338 --- /dev/null +++ b/src/dbus_api/filesystem/filesystem_3_6/props.rs @@ -0,0 +1,57 @@ +// 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::{Iter, IterAppend}; +use dbus_tree::{MTSync, MethodErr, PropInfo}; + +use devicemapper::Bytes; + +use crate::{ + dbus_api::{ + consts, + filesystem::shared::{self, get_filesystem_property}, + types::TData, + util::tuple_to_option, + }, + engine::PropChangeAction, +}; + +pub fn get_fs_size_limit( + i: &mut IterAppend<'_>, + p: &PropInfo<'_, MTSync, TData>, +) -> Result<(), MethodErr> { + get_filesystem_property(i, p, |(_, _, f)| Ok(shared::fs_size_limit_prop(f))) +} + +pub fn set_fs_size_limit( + i: &mut Iter<'_>, + p: &PropInfo<'_, MTSync, TData>, +) -> Result<(), MethodErr> { + let size_limit_opt: (bool, &str) = i.get().ok_or_else(|| { + MethodErr::failed("New filesystem limit required as argument to increase it") + })?; + let size_limit_str = tuple_to_option(size_limit_opt); + let size_limit = match size_limit_str { + Some(lim) => Some(Bytes(lim.parse::().map_err(|e| { + MethodErr::failed(&format!("Failed to parse {lim} as unsigned integer: {e}")) + })?)), + None => None, + }; + + let res = shared::set_fs_property_to_display( + p, + consts::FILESYSTEM_SIZE_LIMIT_PROP, + |(_, uuid, p)| shared::set_fs_size_limit_prop(uuid, p, size_limit), + ); + match res { + Ok(PropChangeAction::NewValue(v)) => { + p.tree + .get_data() + .push_fs_size_limit_change(p.path.get_name(), v); + Ok(()) + } + Ok(PropChangeAction::Identity) => Ok(()), + Err(e) => Err(e), + } +} diff --git a/src/dbus_api/filesystem/mod.rs b/src/dbus_api/filesystem/mod.rs index 5eeb0c291a..411daa173b 100644 --- a/src/dbus_api/filesystem/mod.rs +++ b/src/dbus_api/filesystem/mod.rs @@ -14,6 +14,7 @@ use crate::{ }; mod filesystem_3_0; +mod filesystem_3_6; pub mod prop_conv; mod shared; @@ -110,7 +111,8 @@ pub fn create_dbus_filesystem<'a>( .add_p(filesystem_3_0::uuid_property(&f)) .add_p(filesystem_3_0::created_property(&f)) .add_p(filesystem_3_0::size_property(&f)) - .add_p(filesystem_3_0::used_property(&f)), + .add_p(filesystem_3_0::used_property(&f)) + .add_p(filesystem_3_6::size_limit_property(&f)), ); let path = object_path.get_name().to_owned(); @@ -189,7 +191,8 @@ pub fn get_fs_properties( consts::FILESYSTEM_POOL_PROP => parent, consts::FILESYSTEM_CREATED_PROP => shared::fs_created_prop(fs), consts::FILESYSTEM_SIZE_PROP => shared::fs_size_prop(fs), - consts::FILESYSTEM_USED_PROP => shared::fs_used_prop(fs) + consts::FILESYSTEM_USED_PROP => shared::fs_used_prop(fs), + consts::FILESYSTEM_SIZE_LIMIT_PROP => shared::fs_size_limit_prop(fs) } } } diff --git a/src/dbus_api/filesystem/prop_conv.rs b/src/dbus_api/filesystem/prop_conv.rs index 1b52528220..e64b458426 100644 --- a/src/dbus_api/filesystem/prop_conv.rs +++ b/src/dbus_api/filesystem/prop_conv.rs @@ -2,7 +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 devicemapper::Bytes; +use devicemapper::{Bytes, Sectors}; use crate::dbus_api::util::option_to_tuple; @@ -17,3 +17,9 @@ pub fn fs_size_to_prop(size: Bytes) -> String { pub fn fs_used_to_prop(used: Option) -> (bool, String) { option_to_tuple(used.map(|u| (*u).to_string()), String::new()) } + +/// Generate D-Bus representation of filesystem size limit property. +#[inline] +pub fn fs_size_limit_to_prop(limit: Option) -> (bool, String) { + option_to_tuple(limit.map(|u| (*u.bytes()).to_string()), String::new()) +} diff --git a/src/dbus_api/filesystem/shared.rs b/src/dbus_api/filesystem/shared.rs index 1dd65b440d..f2520ecffb 100644 --- a/src/dbus_api/filesystem/shared.rs +++ b/src/dbus_api/filesystem/shared.rs @@ -3,18 +3,20 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/. use chrono::SecondsFormat; -use dbus::Path; -use dbus_tree::{MTSync, Tree}; +use dbus::{arg::IterAppend, Path}; +use dbus_tree::{MTSync, MethodErr, PropInfo, Tree}; use futures::executor::block_on; +use devicemapper::{Bytes, Sectors}; + use crate::{ dbus_api::{filesystem::prop_conv, types::TData}, - engine::{Filesystem, Name, PoolIdentifier}, + engine::{Filesystem, FilesystemUuid, Name, Pool, PoolIdentifier, PropChangeAction, ToDisplay}, }; /// Get execute a given closure providing a filesystem object and return /// the calculated value -pub fn filesystem_operation( +pub fn filesystem_get_operation( tree: &Tree, TData>, object_path: &Path<'static>, closure: F, @@ -61,6 +63,116 @@ where closure((pool_name, fs_name, fs)) } +/// Get a filesystem property and place it on the D-Bus. The property is +/// found by means of the getter method which takes a reference to a +/// Filesystem and obtains the property from the filesystem. +pub fn get_filesystem_property( + i: &mut IterAppend<'_>, + p: &PropInfo<'_, MTSync, TData>, + getter: F, +) -> Result<(), MethodErr> +where + F: Fn((Name, Name, &dyn Filesystem)) -> Result, + R: dbus::arg::Append, +{ + i.append( + filesystem_get_operation(p.tree, p.path.get_name(), getter) + .map_err(|ref e| MethodErr::failed(e))?, + ); + Ok(()) +} + +/// Set a filesystem property. The property is set by means of +/// the setter method which takes a mutable reference to a +/// Pool for MDV update purposes. +pub fn fs_set_operation( + tree: &Tree, TData>, + object_path: &dbus::Path<'static>, + closure: F, +) -> Result +where + F: Fn((Name, FilesystemUuid, &mut dyn Pool)) -> Result, +{ + let dbus_context = tree.get_data(); + + let fs_path = tree + .get(object_path) + .expect("implicit argument must be in tree"); + let fs_uuid = typed_uuid_string_err!( + fs_path + .get_data() + .as_ref() + .ok_or_else(|| format!("no data for object path {object_path}"))? + .uuid; + Fs + ); + let pool_path = tree + .get( + &fs_path + .get_data() + .as_ref() + .ok_or_else(|| format!("no data for object path {object_path}"))? + .parent, + ) + .ok_or_else(|| "Parent not found in tree".to_string())?; + let pool_uuid = typed_uuid_string_err!( + pool_path + .get_data() + .as_ref() + .ok_or_else(|| format!("no data for object path {object_path}"))? + .uuid; + Pool + ); + + let mut guard = block_on( + dbus_context + .engine + .get_mut_pool(PoolIdentifier::Uuid(pool_uuid)), + ) + .ok_or_else(|| format!("no pool corresponding to uuid {pool_uuid}"))?; + let (name, _) = guard + .get_filesystem(fs_uuid) + .ok_or_else(|| format!("no filesystem corresponding to uuid {fs_uuid}"))?; + + closure((name, fs_uuid, &mut *guard)) +} + +/// Set a filesystem property. The property is found by means of the setter method which +/// takes a mutable reference to a Filesystem and sets the property on the filesystem. +pub fn set_fs_property_to_display( + p: &PropInfo<'_, MTSync, TData>, + prop_name: &str, + setter: F, +) -> Result +where + F: Fn((Name, FilesystemUuid, &mut dyn Pool)) -> Result, + R: ToDisplay, +{ + info!("Setting property {}", prop_name); + let res = + fs_set_operation(p.tree, p.path.get_name(), setter).map_err(|ref e| MethodErr::failed(e)); + let display = res.as_ref().map(|r| r.to_display()); + let _ = handle_action!(display); + res +} + +/// Get the filesystem size limit for a given filesystem. +#[inline] +pub fn fs_size_limit_prop(fs: &dyn Filesystem) -> (bool, String) { + prop_conv::fs_size_limit_to_prop(fs.size_limit()) +} + +/// Set the filesystem size limit for a given filesystem. +#[inline] +pub fn set_fs_size_limit_prop( + uuid: FilesystemUuid, + pool: &mut dyn Pool, + limit: Option, +) -> Result>, String> { + pool.set_fs_size_limit(uuid, limit) + .map_err(|e| e.to_string()) +} + /// Generate D-Bus representation of name property. #[inline] pub fn fs_name_prop(name: &Name) -> String { diff --git a/src/dbus_api/pool/mod.rs b/src/dbus_api/pool/mod.rs index 297668a33e..7f0d614036 100644 --- a/src/dbus_api/pool/mod.rs +++ b/src/dbus_api/pool/mod.rs @@ -17,6 +17,7 @@ mod pool_3_0; mod pool_3_1; mod pool_3_3; mod pool_3_5; +mod pool_3_6; pub mod prop_conv; mod shared; @@ -213,7 +214,7 @@ pub fn create_dbus_pool<'a>( ) .add( f.interface(consts::POOL_INTERFACE_NAME_3_6, ()) - .add_m(pool_3_0::create_filesystems_method(&f)) + .add_m(pool_3_6::create_filesystems_method(&f)) .add_m(pool_3_0::destroy_filesystems_method(&f)) .add_m(pool_3_0::snapshot_filesystem_method(&f)) .add_m(pool_3_0::add_blockdevs_method(&f)) diff --git a/src/dbus_api/pool/pool_3_0/methods.rs b/src/dbus_api/pool/pool_3_0/methods.rs index b9baa0cb98..3a4214335a 100644 --- a/src/dbus_api/pool/pool_3_0/methods.rs +++ b/src/dbus_api/pool/pool_3_0/methods.rs @@ -66,9 +66,9 @@ pub fn create_filesystems(m: &MethodInfo<'_, MTSync, TData>) -> MethodRes }) }) .transpose() - .map(|size_opt| (name, size_opt.map(Bytes))) + .map(|size_opt| (name, size_opt.map(Bytes), None)) }) - .collect::)>, String>>() + .collect::, Option)>, String>>() { Ok(val) => val, Err(err) => { diff --git a/src/dbus_api/pool/pool_3_6/api.rs b/src/dbus_api/pool/pool_3_6/api.rs new file mode 100644 index 0000000000..4c2a8f0ee8 --- /dev/null +++ b/src/dbus_api/pool/pool_3_6/api.rs @@ -0,0 +1,21 @@ +// 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::{Factory, MTSync, Method}; + +use crate::dbus_api::{pool::pool_3_6::methods::create_filesystems, types::TData}; + +pub fn create_filesystems_method( + f: &Factory, TData>, +) -> Method, TData> { + f.method("CreateFilesystems", (), create_filesystems) + .in_arg(("specs", "a(s(bs)(bs))")) + // b: true if filesystems were created + // a(os): Array of tuples with object paths and names + // + // Rust representation: (bool, Vec<(dbus::Path, String)>) + .out_arg(("results", "(ba(os))")) + .out_arg(("return_code", "q")) + .out_arg(("return_string", "s")) +} diff --git a/src/dbus_api/pool/pool_3_6/methods.rs b/src/dbus_api/pool/pool_3_6/methods.rs new file mode 100644 index 0000000000..155b80d633 --- /dev/null +++ b/src/dbus_api/pool/pool_3_6/methods.rs @@ -0,0 +1,130 @@ +// 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::Array, Message}; +use dbus_tree::{MTSync, MethodInfo, MethodResult}; + +use devicemapper::Bytes; + +use crate::{ + dbus_api::{ + filesystem::create_dbus_filesystem, + types::{DbusErrorEnum, TData, OK_STRING}, + util::{engine_to_dbus_err_tuple, get_next_arg, tuple_to_option}, + }, + engine::{EngineAction, Name}, +}; + +type FilesystemSpec<'a> = (&'a str, (bool, &'a str), (bool, &'a str)); + +pub fn create_filesystems(m: &MethodInfo<'_, MTSync, TData>) -> MethodResult { + let message: &Message = m.msg; + let mut iter = message.iter_init(); + + let filesystems: Array<'_, FilesystemSpec<'_>, _> = get_next_arg(&mut iter, 0)?; + let dbus_context = m.tree.get_data(); + + let object_path = m.path.get_name(); + let return_message = message.method_return(); + let default_return: (bool, Vec<(dbus::Path<'_>, &str)>) = (false, Vec::new()); + + if filesystems.count() > 1 { + let error_message = "only 1 filesystem per request allowed"; + let (rc, rs) = (DbusErrorEnum::ERROR as u16, error_message); + return Ok(vec![return_message.append3(default_return, rc, rs)]); + } + + let pool_path = m + .tree + .get(object_path) + .expect("implicit argument must be in tree"); + let pool_uuid = typed_uuid!( + get_data!(pool_path; default_return; return_message).uuid; + Pool; + default_return; + return_message + ); + + let mut guard = get_mut_pool!(dbus_context.engine; pool_uuid; default_return; return_message); + let (pool_name, _, pool) = guard.as_mut_tuple(); + + let filesystem_specs = match filesystems + .map(|(name, size_opt, size_limit_opt)| { + let size = tuple_to_option(size_opt) + .map(|val| { + val.parse::().map_err(|_| { + format!("Could not parse filesystem size string {val} to integer value") + }) + }) + .transpose()?; + let size_limit = tuple_to_option(size_limit_opt) + .map(|val| { + val.parse::().map_err(|_| { + format!( + "Could not parse filesystem size limit string {val} to integer value" + ) + }) + }) + .transpose()?; + Ok((name, size.map(Bytes), size_limit.map(Bytes))) + }) + .collect::, Option)>, String>>() + { + Ok(val) => val, + Err(err) => { + let (rc, rs) = (DbusErrorEnum::ERROR as u16, err); + return Ok(vec![return_message.append3(default_return, rc, rs)]); + } + }; + + let result = handle_action!( + pool.create_filesystems(&pool_name, pool_uuid, &filesystem_specs), + dbus_context, + pool_path.get_name() + ); + + let infos = match result { + Ok(created_set) => created_set.changed(), + Err(err) => { + let (rc, rs) = engine_to_dbus_err_tuple(&err); + return Ok(vec![return_message.append3(default_return, rc, rs)]); + } + }; + + let return_value = match infos { + Some(ref newly_created_filesystems) => { + let v = newly_created_filesystems + .iter() + .map(|&(name, uuid, _)| { + let filesystem = pool + .get_filesystem(uuid) + .expect("just inserted by create_filesystems") + .1; + // FIXME: To avoid this expect, modify create_filesystem + // so that it returns a mutable reference to the + // filesystem created. + ( + create_dbus_filesystem( + dbus_context, + object_path.clone(), + &pool_name, + &Name::new(name.to_string()), + uuid, + filesystem, + ), + name, + ) + }) + .collect::>(); + (true, v) + } + None => default_return, + }; + + Ok(vec![return_message.append3( + return_value, + DbusErrorEnum::OK as u16, + OK_STRING.to_string(), + )]) +} diff --git a/src/dbus_api/pool/pool_3_6/mod.rs b/src/dbus_api/pool/pool_3_6/mod.rs new file mode 100644 index 0000000000..b868ee708c --- /dev/null +++ b/src/dbus_api/pool/pool_3_6/mod.rs @@ -0,0 +1,8 @@ +// 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/. + +mod api; +mod methods; + +pub use api::create_filesystems_method; diff --git a/src/dbus_api/tree.rs b/src/dbus_api/tree.rs index 130525399d..960d241061 100644 --- a/src/dbus_api/tree.rs +++ b/src/dbus_api/tree.rs @@ -30,7 +30,7 @@ use crate::{ blockdev_user_info_to_prop, }, consts, - filesystem::prop_conv::{fs_size_to_prop, fs_used_to_prop}, + filesystem::prop_conv::{fs_size_limit_to_prop, fs_size_to_prop, fs_used_to_prop}, pool::prop_conv::{ avail_actions_to_prop, clevis_info_to_prop, key_desc_to_prop, pool_alloc_to_prop, pool_size_to_prop, pool_used_to_prop, @@ -766,6 +766,26 @@ impl DbusTreeHandler { } } + /// Send a signal indicating that the filesystem size limit has changed. + fn handle_fs_size_limit_change(&self, path: Path<'static>, new_size_limit: Option) { + let size_limit = fs_size_limit_to_prop(new_size_limit); + if let Err(e) = self.property_changed_invalidated_signal( + &path, + prop_hashmap!( + consts::FILESYSTEM_INTERFACE_NAME_3_6 => { + Vec::new(), + consts::FILESYSTEM_SIZE_LIMIT_PROP.to_string() => + box_variant!(size_limit) + } + ), + ) { + warn!( + "Failed to send a signal over D-Bus indicating filesystem size limit change: {}", + e + ); + } + } + /// Send a signal indicating that the blockdev user info has changed. fn handle_blockdev_user_info_change(&self, path: Path<'static>, new_user_info: Option) { let user_info_prop = blockdev_user_info_to_prop(new_user_info); @@ -1105,6 +1125,10 @@ impl DbusTreeHandler { self.handle_pool_fs_limit_change(path, new_limit); Ok(true) } + DbusAction::FsSizeLimitChange(path, new_limit) => { + self.handle_fs_size_limit_change(path, new_limit); + Ok(true) + } DbusAction::PoolOverprovModeChange(path, new_mode) => { self.handle_pool_overprov_mode_change(path, new_mode); Ok(true) diff --git a/src/dbus_api/types.rs b/src/dbus_api/types.rs index 0e50a77681..256834a99f 100644 --- a/src/dbus_api/types.rs +++ b/src/dbus_api/types.rs @@ -106,6 +106,7 @@ pub enum DbusAction { StoppedPoolsChange(StoppedPoolsInfo), BlockdevUserInfoChange(Path<'static>, Option), BlockdevTotalPhysicalSizeChange(Path<'static>, Sectors), + FsSizeLimitChange(Path<'static>, Option), FsBackgroundChange( FilesystemUuid, SignalChange>, @@ -365,6 +366,19 @@ impl DbusContext { } } + /// Send changed signal for pool SizeLimit property. + pub fn push_fs_size_limit_change(&self, item: &Path<'static>, new_size_limit: Option) { + if let Err(e) = self + .sender + .send(DbusAction::FsSizeLimitChange(item.clone(), new_size_limit)) + { + warn!( + "D-Bus filesystem size limit change event could not be sent to the processing thread; no signal will be sent out for the size limit change of filesystem with path {}: {}", + item, e, + ) + } + } + /// Send changed signal for pool overprovisioning mode property. pub fn push_pool_overprov_mode_change(&self, item: &Path<'static>, new_mode: bool) { if let Err(e) = self diff --git a/src/engine/engine.rs b/src/engine/engine.rs index 31dfb74cd6..c6702c7f11 100644 --- a/src/engine/engine.rs +++ b/src/engine/engine.rs @@ -31,7 +31,7 @@ use crate::{ stratis::StratisResult, }; -use super::types::StratBlockDevDiff; +use super::{types::StratBlockDevDiff, PropChangeAction}; pub const DEV_PATH: &str = "/dev/stratis"; /// The maximum size of pool passphrases stored in the kernel keyring @@ -90,6 +90,9 @@ pub trait Filesystem: Debug { /// Get the size of the filesystem in bytes. fn size(&self) -> Bytes; + + /// Get filesystem size limit. + fn size_limit(&self) -> Option; } pub trait BlockDev: Debug { @@ -154,7 +157,7 @@ pub trait Pool: Debug + Send + Sync { &mut self, pool_name: &str, pool_uuid: PoolUuid, - specs: &[(&'b str, Option)], + specs: &[(&'b str, Option, Option)], ) -> StratisResult>; /// Adds blockdevs specified by paths to pool. @@ -328,6 +331,13 @@ pub trait Pool: Debug + Send + Sync { pool_uuid: PoolUuid, device: DevUuid, ) -> StratisResult<(GrowAction<(PoolUuid, DevUuid)>, Option)>; + + /// Set filesystem size limit. + fn set_fs_size_limit( + &mut self, + fs: FilesystemUuid, + limit: Option, + ) -> StratisResult>>; } pub type HandleEvents

= ( diff --git a/src/engine/shared.rs b/src/engine/shared.rs index 83e3d856db..10be45abfa 100644 --- a/src/engine/shared.rs +++ b/src/engine/shared.rs @@ -32,9 +32,6 @@ const DEFAULT_THIN_DEV_SIZE: Sectors = Sectors(2 * IEC::Gi); // 1 TiB #[cfg(test)] pub const DEFAULT_THIN_DEV_SIZE: Sectors = Sectors(2 * IEC::Gi); // 1 TiB -// Maximum taken from "XFS Algorithms and Data Structured: 3rd edition" -const MAX_THIN_DEV_SIZE: Sectors = Sectors(16 * IEC::Pi); // 8 EiB - // xfs is planning to reject making "small" filesystems: // https://www.spinics.net/lists/linux-xfs/msg59453.html const MIN_THIN_DEV_SIZE: Sectors = Sectors(IEC::Mi); // 512 MiB @@ -205,36 +202,40 @@ pub fn validate_paths(paths: &[&Path]) -> StratisResult<()> { } } +pub fn validate_filesystem_size( + name: &str, + size_opt: Option, +) -> StratisResult> { + size_opt + .map(|size| { + let size_sectors = size.sectors(); + if size_sectors.bytes() != size { + Err(StratisError::Msg(format!( + "Requested size or size limit of filesystem {name} must be divisble by {SECTOR_SIZE}" + ))) + } else if size_sectors < MIN_THIN_DEV_SIZE { + Err(StratisError::Msg(format!( + "Requested size or size_limit of filesystem {name} is {size_sectors} which is less than minimum required: {MIN_THIN_DEV_SIZE}" + ))) + } else { + Ok(size_sectors) + } + }) + .transpose() +} + pub fn validate_filesystem_size_specs<'a>( - specs: &[(&'a str, Option)], -) -> StratisResult> { + specs: &[(&'a str, Option, Option)], +) -> StratisResult)>> { specs .iter() - .map(|&(name, size_opt)| { - size_opt - .map(|size| { - let size_sectors = size.sectors(); - if size_sectors.bytes() != size { - Err(StratisError::Msg(format!( - "Requested size of filesystem {name} must be divisble by {SECTOR_SIZE}" - ))) - } else if size_sectors < MIN_THIN_DEV_SIZE { - Err(StratisError::Msg(format!( - "Requested size of filesystem {name} is {size_sectors} which is less than minimum required: {MIN_THIN_DEV_SIZE}" - ))) - } else if size_sectors > MAX_THIN_DEV_SIZE { - Err(StratisError::Msg(format!( - "Requested size of filesystem {name} is {size_sectors} which is greater than maximum allowed: {MAX_THIN_DEV_SIZE}" - ))) - } else { - Ok(size_sectors) - } - }) - .transpose() - .map(|size_opt| size_opt.unwrap_or(DEFAULT_THIN_DEV_SIZE)) - .map(|size| (name, size)) + .map(|&(name, size_opt, size_limit_opt)| { + let size = validate_filesystem_size(name, size_opt) + .map(|size_opt| size_opt.unwrap_or(DEFAULT_THIN_DEV_SIZE))?; + let size_limit = validate_filesystem_size(name, size_limit_opt)?; + Ok((name, (size, size_limit))) }) - .collect::>>() + .collect::)>>>() } /// Gather a collection of information from block devices that may or may not diff --git a/src/engine/sim_engine/engine.rs b/src/engine/sim_engine/engine.rs index 70f983df06..68e544aa58 100644 --- a/src/engine/sim_engine/engine.rs +++ b/src/engine/sim_engine/engine.rs @@ -445,7 +445,7 @@ mod tests { .unwrap(); { let mut pool = test_async!(engine.get_mut_pool(PoolIdentifier::Uuid(uuid))).unwrap(); - pool.create_filesystems(pool_name, uuid, &[("test", None)]) + pool.create_filesystems(pool_name, uuid, &[("test", None, None)]) .unwrap(); } assert!(test_async!(engine.destroy_pool(uuid)).is_err()); diff --git a/src/engine/sim_engine/filesystem.rs b/src/engine/sim_engine/filesystem.rs index 3f99507395..9309075181 100644 --- a/src/engine/sim_engine/filesystem.rs +++ b/src/engine/sim_engine/filesystem.rs @@ -9,27 +9,58 @@ use serde_json::{Map, Value}; use devicemapper::{Bytes, Sectors}; -use crate::{engine::Filesystem, stratis::StratisResult}; +use crate::{ + engine::Filesystem, + stratis::{StratisError, StratisResult}, +}; #[derive(Debug)] pub struct SimFilesystem { rand: u32, created: DateTime, size: Sectors, + size_limit: Option, } impl SimFilesystem { - pub fn new(size: Sectors) -> SimFilesystem { - SimFilesystem { + pub fn new(size: Sectors, size_limit: Option) -> StratisResult { + if let Some(limit) = size_limit { + if limit < size { + return Err(StratisError::Msg(format!( + "Limit of {limit} is less than requested size {size}" + ))); + } + } + Ok(SimFilesystem { rand: rand::random::(), created: Utc::now(), size, - } + size_limit, + }) } pub fn size(&self) -> Sectors { self.size } + + /// Set the size limit for the SimFilesystem. + pub fn set_size_limit(&mut self, limit: Option) -> StratisResult { + match limit { + Some(lim) if self.size() > lim => Err(StratisError::Msg(format!( + "Limit requested of {} is smaller than current filesystem size of {}", + lim, + self.size() + ))), + Some(_) | None => { + if self.size_limit == limit { + Ok(false) + } else { + self.size_limit = limit; + Ok(true) + } + } + } + } } impl Filesystem for SimFilesystem { @@ -54,6 +85,10 @@ impl Filesystem for SimFilesystem { fn size(&self) -> Bytes { self.size.bytes() } + + fn size_limit(&self) -> Option { + self.size_limit + } } impl<'a> Into for &'a SimFilesystem { @@ -68,6 +103,15 @@ impl<'a> Into for &'a SimFilesystem { .unwrap_or_else(|_| "Unavailable".to_string()), ), ); + json.insert( + "size_limit".to_string(), + Value::from( + self.size_limit + .as_ref() + .map(|v| v.to_string()) + .unwrap_or_else(|| "Not set".to_string()), + ), + ); Value::from(json) } } diff --git a/src/engine/sim_engine/pool.rs b/src/engine/sim_engine/pool.rs index cc4f258f84..d6b7dda5c9 100644 --- a/src/engine/sim_engine/pool.rs +++ b/src/engine/sim_engine/pool.rs @@ -16,8 +16,8 @@ use crate::{ engine::{ engine::{BlockDev, Filesystem, Pool}, shared::{ - gather_encryption_info, init_cache_idempotent_or_err, validate_filesystem_size_specs, - validate_name, validate_paths, + gather_encryption_info, init_cache_idempotent_or_err, validate_filesystem_size, + validate_filesystem_size_specs, validate_name, validate_paths, }, sim_engine::{blockdev::SimDev, filesystem::SimFilesystem}, structures::Table, @@ -27,6 +27,7 @@ use crate::{ PoolEncryptionInfo, PoolUuid, RegenAction, RenameAction, SetCreateAction, SetDeleteAction, }, + PropChangeAction, }, stratis::{StratisError, StratisResult}, }; @@ -212,13 +213,13 @@ impl Pool for SimPool { &mut self, _pool_name: &str, _pool_uuid: PoolUuid, - specs: &[(&'b str, Option)], + specs: &[(&'b str, Option, Option)], ) -> StratisResult> { self.check_fs_limit(specs.len())?; let spec_map = validate_filesystem_size_specs(specs)?; - spec_map.iter().try_fold((), |_, (name, size)| { + spec_map.iter().try_fold((), |_, (name, (size, _))| { validate_name(name) .and_then(|()| { if let Some((_, fs)) = self.filesystems.get_by_name(name) { @@ -239,10 +240,10 @@ impl Pool for SimPool { })?; let mut result = Vec::new(); - for (name, size) in spec_map { + for (name, (size, size_limit)) in spec_map { if !self.filesystems.contains_name(name) { let uuid = FilesystemUuid::new_v4(); - let new_filesystem = SimFilesystem::new(size); + let new_filesystem = SimFilesystem::new(size, size_limit)?; self.filesystems .insert(Name::new((name).to_owned()), uuid, new_filesystem); result.push((name, uuid, size)); @@ -532,7 +533,7 @@ impl Pool for SimPool { return Ok(CreateAction::Identity); } } - SimFilesystem::new(filesystem.size()) + SimFilesystem::new(filesystem.size(), filesystem.size_limit())? } None => { return Err(StratisError::Msg(origin_uuid.to_string())); @@ -691,6 +692,23 @@ impl Pool for SimPool { ) -> StratisResult<(GrowAction<(PoolUuid, DevUuid)>, Option)> { Ok((GrowAction::Identity, None)) } + + fn set_fs_size_limit( + &mut self, + fs_uuid: FilesystemUuid, + limit: Option, + ) -> StratisResult>> { + let (name, fs) = self.filesystems.get_mut_by_uuid(fs_uuid).ok_or_else(|| { + StratisError::Msg(format!("Filesystem with UUID {fs_uuid} not found")) + })?; + let limit = validate_filesystem_size(&name, limit)?; + let changed = fs.set_size_limit(limit)?; + if changed { + Ok(PropChangeAction::NewValue(limit)) + } else { + Ok(PropChangeAction::Identity) + } + } } #[cfg(test)] @@ -741,7 +759,7 @@ mod tests { .unwrap(); let mut pool = test_async!(engine.get_mut_pool(PoolIdentifier::Uuid(uuid))).unwrap(); let infos = pool - .create_filesystems(pool_name, uuid, &[("old_name", None)]) + .create_filesystems(pool_name, uuid, &[("old_name", None, None)]) .unwrap() .changed() .unwrap(); @@ -769,7 +787,11 @@ mod tests { .unwrap(); let mut pool = test_async!(engine.get_mut_pool(PoolIdentifier::Uuid(uuid))).unwrap(); let results = pool - .create_filesystems(pool_name, uuid, &[(old_name, None), (new_name, None)]) + .create_filesystems( + pool_name, + uuid, + &[(old_name, None, None), (new_name, None, None)], + ) .unwrap() .changed() .unwrap(); @@ -856,7 +878,7 @@ mod tests { .unwrap(); let mut pool = test_async!(engine.get_mut_pool(PoolIdentifier::Uuid(uuid))).unwrap(); let fs_results = pool - .create_filesystems(pool_name, uuid, &[("fs_name", None)]) + .create_filesystems(pool_name, uuid, &[("fs_name", None, None)]) .unwrap() .changed() .unwrap(); @@ -900,7 +922,7 @@ mod tests { .unwrap(); let mut pool = test_async!(engine.get_mut_pool(PoolIdentifier::Uuid(uuid))).unwrap(); assert!(match pool - .create_filesystems(pool_name, uuid, &[("name", None)]) + .create_filesystems(pool_name, uuid, &[("name", None, None)]) .ok() .and_then(|fs| fs.changed()) { @@ -924,10 +946,10 @@ mod tests { .changed() .unwrap(); let mut pool = test_async!(engine.get_mut_pool(PoolIdentifier::Uuid(uuid))).unwrap(); - pool.create_filesystems(pool_name, uuid, &[(fs_name, None)]) + pool.create_filesystems(pool_name, uuid, &[(fs_name, None, None)]) .unwrap(); let set_create_action = pool - .create_filesystems(pool_name, uuid, &[(fs_name, None)]) + .create_filesystems(pool_name, uuid, &[(fs_name, None, None)]) .unwrap(); assert!(!set_create_action.is_changed()); } @@ -948,7 +970,11 @@ mod tests { .unwrap(); let mut pool = test_async!(engine.get_mut_pool(PoolIdentifier::Uuid(uuid))).unwrap(); assert!(match pool - .create_filesystems(pool_name, uuid, &[(fs_name, None), (fs_name, None)]) + .create_filesystems( + pool_name, + uuid, + &[(fs_name, None, None), (fs_name, None, None)] + ) .ok() .and_then(|fs| fs.changed()) { diff --git a/src/engine/strat_engine/engine.rs b/src/engine/strat_engine/engine.rs index 100f551072..3c607e9652 100644 --- a/src/engine/strat_engine/engine.rs +++ b/src/engine/strat_engine/engine.rs @@ -788,12 +788,12 @@ mod test { let (fs_uuid1, fs_uuid2) = { let mut pool = test_async!(engine.get_mut_pool(PoolIdentifier::Uuid(uuid1))).unwrap(); let fs_uuid1 = pool - .create_filesystems(name1, uuid1, &[(fs_name1, None)]) + .create_filesystems(name1, uuid1, &[(fs_name1, None, None)]) .unwrap() .changed() .unwrap(); let fs_uuid2 = pool - .create_filesystems(name1, uuid1, &[(fs_name2, None)]) + .create_filesystems(name1, uuid1, &[(fs_name2, None, None)]) .unwrap() .changed() .unwrap(); diff --git a/src/engine/strat_engine/pool.rs b/src/engine/strat_engine/pool.rs index 4f9cdb83df..4a8d1ee930 100644 --- a/src/engine/strat_engine/pool.rs +++ b/src/engine/strat_engine/pool.rs @@ -14,8 +14,8 @@ use crate::{ engine::{ engine::{BlockDev, DumpState, Filesystem, Pool, StateDiff}, shared::{ - init_cache_idempotent_or_err, validate_filesystem_size_specs, validate_name, - validate_paths, + init_cache_idempotent_or_err, validate_filesystem_size, validate_filesystem_size_specs, + validate_name, validate_paths, }, strat_engine::{ backstore::{Backstore, ProcessedPathInfos, StratBlockDev, UnownedDevices}, @@ -32,6 +32,7 @@ use crate::{ PoolEncryptionInfo, PoolUuid, RegenAction, RenameAction, SetCreateAction, SetDeleteAction, StratFilesystemDiff, StratPoolDiff, }, + PropChangeAction, }, stratis::{StratisError, StratisResult}, }; @@ -755,16 +756,20 @@ impl Pool for StratPool { &mut self, pool_name: &str, pool_uuid: PoolUuid, - specs: &[(&'a str, Option)], + specs: &[(&'a str, Option, Option)], ) -> StratisResult> { self.check_fs_limit(specs.len())?; let spec_map = validate_filesystem_size_specs(specs)?; - let increase = spec_map.values().copied().sum::(); + let increase = spec_map + .values() + .map(|(size, _)| size) + .copied() + .sum::(); self.check_overprov(increase)?; - spec_map.iter().try_fold((), |_, (name, size)| { + spec_map.iter().try_fold((), |_, (name, (size, _))| { validate_name(name) .and_then(|()| { if let Some((_, fs)) = self.thin_pool.get_filesystem_by_name(name) { @@ -786,11 +791,11 @@ impl Pool for StratPool { // TODO: Roll back on filesystem initialization failure. let mut result = Vec::new(); - for (name, size) in spec_map { + for (name, (size, size_limit)) in spec_map { if self.thin_pool.get_mut_filesystem_by_name(name).is_none() { let fs_uuid = self .thin_pool - .create_filesystem(pool_name, pool_uuid, name, size)?; + .create_filesystem(pool_name, pool_uuid, name, size, size_limit)?; result.push((name, fs_uuid, size)); } } @@ -1219,6 +1224,23 @@ impl Pool for StratPool { Ok((GrowAction::Identity, None)) } } + + #[pool_mutating_action("NoRequests")] + fn set_fs_size_limit( + &mut self, + fs_uuid: FilesystemUuid, + limit: Option, + ) -> StratisResult>> { + let (name, _) = self.get_filesystem(fs_uuid).ok_or_else(|| { + StratisError::Msg(format!("Filesystem with UUID {fs_uuid} not found")) + })?; + let limit = validate_filesystem_size(&name, limit)?; + if self.thin_pool.set_fs_size_limit(fs_uuid, limit)? { + Ok(PropChangeAction::NewValue(limit)) + } else { + Ok(PropChangeAction::Identity) + } + } } pub struct StratPoolState { @@ -1331,7 +1353,7 @@ mod tests { assert_matches!(metadata1.backstore.cache_tier, None); let (_, fs_uuid, _) = pool - .create_filesystems(name, uuid, &[("stratis-filesystem", None)]) + .create_filesystems(name, uuid, &[("stratis-filesystem", None, None)]) .unwrap() .changed() .and_then(|mut fs| fs.pop()) @@ -1456,7 +1478,7 @@ mod tests { let fs_name = "stratis_test_filesystem"; let (_, fs_uuid, _) = pool - .create_filesystems(name, pool_uuid, &[(fs_name, None)]) + .create_filesystems(name, pool_uuid, &[(fs_name, None, None)]) .unwrap() .changed() .and_then(|mut fs| fs.pop()) @@ -1600,6 +1622,7 @@ mod tests { &[( "stratis_test_filesystem", Some(pool.backstore.datatier_usable_size().bytes() * 2u64), + None, )], ) .unwrap() @@ -1621,7 +1644,8 @@ mod tests { pool_uuid, &[( "stratis_test_filesystem", - Some(pool.backstore.datatier_usable_size().bytes() * 2u64) + Some(pool.backstore.datatier_usable_size().bytes() * 2u64), + None )], ) .is_err()); @@ -1633,7 +1657,7 @@ mod tests { .create_filesystems( pool_name, pool_uuid, - &[("stratis_test_filesystem", Some(initial_fs_size))], + &[("stratis_test_filesystem", Some(initial_fs_size), None)], ) .unwrap() .changed() @@ -1705,7 +1729,11 @@ mod tests { let (_, _, pool) = guard.as_mut_tuple(); let (_, fs_uuid, _) = pool - .create_filesystems(&pool_name, pool_uuid, &[("stratis_test_filesystem", None)]) + .create_filesystems( + &pool_name, + pool_uuid, + &[("stratis_test_filesystem", None, None)], + ) .unwrap() .changed() .unwrap() diff --git a/src/engine/strat_engine/serde_structs.rs b/src/engine/strat_engine/serde_structs.rs index 913a46e97a..f77693509a 100644 --- a/src/engine/strat_engine/serde_structs.rs +++ b/src/engine/strat_engine/serde_structs.rs @@ -117,4 +117,6 @@ pub struct FilesystemSave { pub thin_id: ThinDevId, pub size: Sectors, pub created: u64, // Unix timestamp + #[serde(skip_serializing_if = "Option::is_none")] + pub fs_size_limit: Option, } diff --git a/src/engine/strat_engine/thinpool/filesystem.rs b/src/engine/strat_engine/thinpool/filesystem.rs index ac85aec876..8248336c65 100644 --- a/src/engine/strat_engine/thinpool/filesystem.rs +++ b/src/engine/strat_engine/thinpool/filesystem.rs @@ -50,6 +50,7 @@ pub struct StratFilesystem { thin_dev: ThinDev, created: DateTime, used: Option, + size_limit: Option, } fn init_used(thin_dev: &ThinDev) -> Option { @@ -71,8 +72,17 @@ impl StratFilesystem { pool_uuid: PoolUuid, thinpool_dev: &ThinPoolDev, size: Sectors, + size_limit: Option, id: ThinDevId, ) -> StratisResult<(FilesystemUuid, StratFilesystem)> { + if let Some(limit) = size_limit { + if limit < size { + return Err(StratisError::Msg(format!( + "Requested limit {limit} is less than requested size {size}", + ))); + } + } + let fs_uuid = FilesystemUuid::new_v4(); let (dm_name, dm_uuid) = format_thin_ids(pool_uuid, ThinRole::Filesystem(fs_uuid)); let mut thin_dev = @@ -103,6 +113,7 @@ impl StratFilesystem { used: init_used(&thin_dev), thin_dev, created: Utc::now(), + size_limit, }, )) } @@ -128,6 +139,7 @@ impl StratFilesystem { used: init_used(&thin_dev), thin_dev, created, + size_limit: fssave.fs_size_limit, }) } @@ -175,6 +187,9 @@ impl StratFilesystem { /// Mounting a filesystem with a duplicate UUID would require special handling, /// so snapshot_fs_uuid is used to update the new snapshot filesystem so it has /// a unique UUID. + /// + /// As of the introduction of filesystem size limits, snapshots inherit the origin size limit + /// but the limit can be changed or removed through the API. #[allow(clippy::too_many_arguments)] pub fn snapshot( &self, @@ -229,6 +244,7 @@ impl StratFilesystem { used: init_used(&thin_dev), thin_dev, created: Utc::now(), + size_limit: self.size_limit, }) } Err(e) => Err(StratisError::Msg(format!( @@ -308,16 +324,24 @@ impl StratFilesystem { } /// Return an extend size for the thindev under the filesystem - pub fn extend_size(current_size: Sectors, remaining_size: Option<&mut Sectors>) -> Sectors { - if let Some(rem_size) = remaining_size { - // Extend either by the remaining amount left before the data device - // overprovisioning limit is reached if it is less than the size of the - // filesystem or double the filesystem size. - let extend_size = min(*rem_size, current_size); - *rem_size -= extend_size; - extend_size - } else { - current_size + pub fn extend_size( + current_size: Sectors, + no_op_remaining_size: Option<&mut Sectors>, + fs_limit_remaining_size: Option, + ) -> Sectors { + match (no_op_remaining_size, fs_limit_remaining_size) { + (Some(no_op_rem_size), Some(fs_lim_rem_size)) => { + let extend_size = min(min(*no_op_rem_size, current_size), fs_lim_rem_size); + *no_op_rem_size -= extend_size; + extend_size + } + (Some(no_op_rem_size), None) => { + let extend_size = min(*no_op_rem_size, current_size); + *no_op_rem_size -= extend_size; + extend_size + } + (None, Some(fs_lim_rem_size)) => min(fs_lim_rem_size, current_size), + (_, _) => current_size, } } @@ -341,6 +365,7 @@ impl StratFilesystem { thin_id: self.thin_dev.id(), size: self.thin_dev.size(), created: self.created.timestamp() as u64, + fs_size_limit: self.size_limit, } } @@ -374,6 +399,24 @@ impl StratFilesystem { Ok(ret_vec) } + pub fn set_size_limit(&mut self, limit: Option) -> StratisResult { + match limit { + Some(lim) if self.thindev_size() > lim => Err(StratisError::Msg(format!( + "Limit requested of {} is smaller than current filesystem size of {}", + lim, + self.thindev_size() + ))), + Some(_) | None => { + if self.size_limit == limit { + Ok(false) + } else { + self.size_limit = limit; + Ok(true) + } + } + } + } + pub fn thindev_size(&self) -> Sectors { self.thin_dev.size() } @@ -412,6 +455,10 @@ impl Filesystem for StratFilesystem { fn size(&self) -> Bytes { self.thin_dev.size().bytes() } + + fn size_limit(&self) -> Option { + self.size_limit + } } /// Represents the state of the Stratis filesystem at a given moment in time. @@ -489,6 +536,15 @@ impl<'a> Into for &'a StratFilesystem { .unwrap_or_else(|_| "Unavailable".to_string()), ), ); + json.insert( + "size_limit".to_string(), + Value::from( + self.size_limit + .as_ref() + .map(|v| v.to_string()) + .unwrap_or_else(|| "Not set".to_string()), + ), + ); Value::from(json) } } diff --git a/src/engine/strat_engine/thinpool/thinpool.rs b/src/engine/strat_engine/thinpool/thinpool.rs index 479641d328..60857dae01 100644 --- a/src/engine/strat_engine/thinpool/thinpool.rs +++ b/src/engine/strat_engine/thinpool/thinpool.rs @@ -23,7 +23,7 @@ use devicemapper::{ use crate::{ engine::{ - engine::{DumpState, StateDiff}, + engine::{DumpState, Filesystem, StateDiff}, strat_engine::{ backstore::Backstore, cmd::{thin_check, thin_metadata_size, thin_repair}, @@ -701,6 +701,7 @@ impl ThinPool { let extend_size = StratFilesystem::extend_size( fs.thindev_size(), remaining_space.as_mut(), + fs.size_limit().map(|sl| sl - fs.thindev_size()), ); if extend_size == Sectors(0) { None @@ -1189,6 +1190,7 @@ impl ThinPool { pool_uuid: PoolUuid, name: &str, size: Sectors, + size_limit: Option, ) -> StratisResult { if self .mdv @@ -1203,8 +1205,13 @@ impl ThinPool { ))); } - let (fs_uuid, mut new_filesystem) = - StratFilesystem::initialize(pool_uuid, &self.thin_pool, size, self.id_gen.new_id()?)?; + let (fs_uuid, mut new_filesystem) = StratFilesystem::initialize( + pool_uuid, + &self.thin_pool, + size, + size_limit, + self.id_gen.new_id()?, + )?; let name = Name::new(name.to_owned()); if let Err(err) = self.mdv.save_fs(&name, fs_uuid, &new_filesystem) { if let Err(err2) = retry_with_index(Fixed::from_millis(100).take(4), |i| { @@ -1555,6 +1562,27 @@ impl ThinPool { pub fn clear_out_of_meta_flag(&mut self) { self.out_of_meta_space = false; } + + /// Set the filesystem size limit for filesystem with given UUID. + pub fn set_fs_size_limit( + &mut self, + fs_uuid: FilesystemUuid, + limit: Option, + ) -> StratisResult { + let changed = { + let (_, fs) = self.get_mut_filesystem_by_uuid(fs_uuid).ok_or_else(|| { + StratisError::Msg(format!("No filesystem with UUID {fs_uuid} found")) + })?; + fs.set_size_limit(limit)? + }; + let (name, fs) = self + .get_filesystem_by_uuid(fs_uuid) + .ok_or_else(|| StratisError::Msg(format!("No filesystem with UUID {fs_uuid} found")))?; + if changed { + self.mdv.save_fs(&name, fs_uuid, fs)?; + } + Ok(changed) + } } impl<'a> Into for &'a ThinPool { @@ -1807,6 +1835,7 @@ mod tests { pool_uuid, format!("testfs{i}").as_str(), Sectors(2 * IEC::Gi), + None, ) .unwrap(); i += 1; @@ -1893,6 +1922,7 @@ mod tests { pool_uuid, "stratis_test_filesystem", DEFAULT_THIN_DEV_SIZE, + None, ) .unwrap(); @@ -2028,7 +2058,13 @@ mod tests { let filesystem_name = "stratis_test_filesystem"; let fs_uuid = pool - .create_filesystem(pool_name, pool_uuid, filesystem_name, DEFAULT_THIN_DEV_SIZE) + .create_filesystem( + pool_name, + pool_uuid, + filesystem_name, + DEFAULT_THIN_DEV_SIZE, + None, + ) .unwrap(); cmd::udev_settle().unwrap(); @@ -2142,7 +2178,7 @@ mod tests { let pool_name = "stratis_test_pool"; let fs_uuid = pool - .create_filesystem(pool_name, pool_uuid, name1, DEFAULT_THIN_DEV_SIZE) + .create_filesystem(pool_name, pool_uuid, name1, DEFAULT_THIN_DEV_SIZE, None) .unwrap(); cmd::udev_settle().unwrap(); @@ -2211,7 +2247,7 @@ mod tests { .unwrap(); let fs_uuid = pool - .create_filesystem(pool_name, pool_uuid, "fsname", DEFAULT_THIN_DEV_SIZE) + .create_filesystem(pool_name, pool_uuid, "fsname", DEFAULT_THIN_DEV_SIZE, None) .unwrap(); let tmp_dir = tempfile::Builder::new() @@ -2287,7 +2323,7 @@ mod tests { let pool_name = "stratis_test_pool"; let fs_name = "stratis_test_filesystem"; let fs_uuid = pool - .create_filesystem(pool_name, pool_uuid, fs_name, DEFAULT_THIN_DEV_SIZE) + .create_filesystem(pool_name, pool_uuid, fs_name, DEFAULT_THIN_DEV_SIZE, None) .unwrap(); retry_operation!(pool.destroy_filesystem(pool_name, fs_uuid)); @@ -2357,6 +2393,7 @@ mod tests { pool_uuid, "stratis_test_filesystem", DEFAULT_THIN_DEV_SIZE, + None, ) .unwrap(); @@ -2419,6 +2456,7 @@ mod tests { pool_uuid, "stratis_test_filesystem", DEFAULT_THIN_DEV_SIZE, + None, ) .unwrap(); @@ -2504,4 +2542,146 @@ mod tests { fn real_test_set_device() { real::test_with_spec(&real::DeviceLimits::AtLeast(2, None, None), test_set_device); } + + /// Set up thinpool and backstore. Set up filesystem and set size limit. + /// Write past the halfway mark of the filesystem and check that the filesystem + /// size limit is respected. Increase the filesystem size limit and check that + /// it is respected. Remove the filesystem size limit and verify that the + /// filesystem size doubles. Verify that the filesystem size limit cannot be set + /// below the current filesystem size. + fn test_fs_size_limit(paths: &[&Path]) { + let pool_name = "pool"; + let pool_uuid = PoolUuid::new_v4(); + + let devices = get_devices(paths).unwrap(); + + let mut backstore = Backstore::initialize( + Name::new(pool_name.to_string()), + pool_uuid, + devices, + MDADataSize::default(), + None, + ) + .unwrap(); + let mut pool = ThinPool::new( + pool_uuid, + &ThinPoolSizeParams::new(backstore.available_in_backstore()).unwrap(), + DATA_BLOCK_SIZE, + &mut backstore, + ) + .unwrap(); + + let fs_uuid = pool + .create_filesystem( + pool_name, + pool_uuid, + "stratis_test_filesystem", + Sectors::from(1200 * IEC::Ki), + // 700 * IEC::Mi + Some(Sectors(1400 * IEC::Ki)), + ) + .unwrap(); + let devnode = { + let (_, fs) = pool.get_mut_filesystem_by_uuid(fs_uuid).unwrap(); + assert_eq!(fs.size_limit(), Some(Sectors(1400 * IEC::Ki))); + fs.devnode() + }; + + let tmp_dir = tempfile::Builder::new() + .prefix("stratis_testing") + .tempdir() + .unwrap(); + let new_file = tmp_dir.path().join("stratis_test.txt"); + mount( + Some(&devnode), + tmp_dir.path(), + Some("xfs"), + MsFlags::empty(), + None as Option<&str>, + ) + .unwrap(); + let mut file = OpenOptions::new() + .create(true) + .write(true) + .open(&new_file) + .unwrap(); + let mut bytes_written = Bytes(0); + // Write 400 * IEC::Mi + while bytes_written < Bytes::from(400 * IEC::Mi) { + file.write_all(&[1; 4096]).unwrap(); + bytes_written += Bytes(4096); + } + file.sync_all().unwrap(); + pool.check_fs(pool_uuid, &backstore).unwrap(); + + { + let (_, fs) = pool.get_mut_filesystem_by_uuid(fs_uuid).unwrap(); + assert_eq!(fs.size_limit(), Some(fs.size().sectors())); + } + + // 800 * IEC::Mi + pool.set_fs_size_limit(fs_uuid, Some(Sectors(1600 * IEC::Ki))) + .unwrap(); + { + let (_, fs) = pool.get_mut_filesystem_by_uuid(fs_uuid).unwrap(); + assert_eq!(fs.size_limit(), Some(Sectors(1600 * IEC::Ki))); + } + let mut bytes_written = Bytes(0); + // Write 100 * IEC::Mi + while bytes_written < Bytes::from(100 * IEC::Mi) { + file.write_all(&[1; 4096]).unwrap(); + bytes_written += Bytes(4096); + } + file.sync_all().unwrap(); + pool.check_fs(pool_uuid, &backstore).unwrap(); + + { + let (_, fs) = pool.get_mut_filesystem_by_uuid(fs_uuid).unwrap(); + assert_eq!(fs.size_limit(), Some(fs.size().sectors())); + } + + { + let (_, fs) = pool + .snapshot_filesystem(pool_name, pool_uuid, fs_uuid, "snapshot") + .unwrap(); + assert_eq!(fs.size_limit(), Some(Sectors(1600 * IEC::Ki))); + } + + pool.set_fs_size_limit(fs_uuid, None).unwrap(); + { + let (_, fs) = pool.get_mut_filesystem_by_uuid(fs_uuid).unwrap(); + assert_eq!(fs.size_limit(), None); + } + let mut bytes_written = Bytes(0); + // Write 200 * IEC::Mi + while bytes_written < Bytes::from(200 * IEC::Mi) { + file.write_all(&[1; 4096]).unwrap(); + bytes_written += Bytes(4096); + } + file.sync_all().unwrap(); + pool.check_fs(pool_uuid, &backstore).unwrap(); + + { + let (_, fs) = pool.get_mut_filesystem_by_uuid(fs_uuid).unwrap(); + assert_eq!(fs.size().sectors(), Sectors(3200 * IEC::Ki)); + } + + assert!(pool.set_fs_size_limit(fs_uuid, Some(Sectors(50))).is_err()); + } + + #[test] + fn loop_test_fs_size_limit() { + loopbacked::test_with_spec( + &loopbacked::DeviceLimits::Range(1, 3, Some(Sectors(10 * IEC::Mi))), + test_fs_size_limit, + ); + } + + #[test] + fn real_test_fs_size_limit() { + real::test_with_spec( + &real::DeviceLimits::Range(1, 3, Some(Sectors(10 * IEC::Mi)), None), + test_fs_size_limit, + ); + } } diff --git a/src/engine/types/actions.rs b/src/engine/types/actions.rs index 8d130ec17f..1e029748d2 100644 --- a/src/engine/types/actions.rs +++ b/src/engine/types/actions.rs @@ -773,7 +773,7 @@ pub enum PropChangeAction { impl ToDisplay for PropChangeAction> where - T: Display, + T: ToString, { type Display = PropChangeAction; @@ -781,7 +781,7 @@ where match self { PropChangeAction::Identity => PropChangeAction::Identity, PropChangeAction::NewValue(Some(v)) => { - PropChangeAction::NewValue(format!("a value of {v}")) + PropChangeAction::NewValue(format!("a value of {}", v.to_string())) } PropChangeAction::NewValue(None) => { PropChangeAction::NewValue("an empty value".to_string()) diff --git a/src/jsonrpc/server/filesystem.rs b/src/jsonrpc/server/filesystem.rs index 361f6e1f1f..8254c18bbe 100644 --- a/src/jsonrpc/server/filesystem.rs +++ b/src/jsonrpc/server/filesystem.rs @@ -26,7 +26,7 @@ pub async fn filesystem_create<'a>( let (_, pool_uuid, pool) = guard.as_mut_tuple(); block_in_place(|| { Ok(pool - .create_filesystems(pool_name, pool_uuid, &[(name, None)])? + .create_filesystems(pool_name, pool_uuid, &[(name, None, None)])? .is_changed()) }) }