From 89df80a2893e3585a6a09a30598aa6aea48033fe Mon Sep 17 00:00:00 2001 From: Christian Hagemeier Date: Thu, 7 Nov 2024 16:24:39 +0000 Subject: [PATCH 1/5] Add statfs implementation Signed-off-by: Christian Hagemeier --- mountpoint-s3/src/fs.rs | 54 +++++++++++++++++++++++++++++++++++++++ mountpoint-s3/src/fuse.rs | 19 +++++++++++++- 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/mountpoint-s3/src/fs.rs b/mountpoint-s3/src/fs.rs index 75efcf862..834772339 100644 --- a/mountpoint-s3/src/fs.rs +++ b/mountpoint-s3/src/fs.rs @@ -107,6 +107,42 @@ pub struct DirectoryEntry { lookup: LookedUp, } +#[derive(Debug)] +/// Reply to a 'statfs' call +pub struct StatFs { + /// Total number of blocks + pub total_blocks: u64, + /// Number of free blocks + pub free_blocks: u64, + /// Number of free blocks available to unprivileged user + pub available_blocks: u64, + /// Number of inodes in file system + pub total_inodes: u64, + /// Available inodes + pub free_inodes: u64, + /// Optimal transfer block size + pub block_size: u32, + /// Maximum name length + pub maximum_name_length: u32, + /// Fragement size + pub fragment_size: u32, +} + +impl Default for StatFs { + fn default() -> Self { + Self { + total_blocks: 0, + free_blocks: 0, + available_blocks: 0, + total_inodes: 0, + free_inodes: 0, + block_size: 512, + maximum_name_length: 255, + fragment_size: 0, + } + } +} + impl S3Filesystem where Client: ObjectClient + Clone + Send + Sync + 'static, @@ -789,6 +825,24 @@ where } Ok(self.superblock.unlink(&self.client, parent_ino, name).await?) } + + pub async fn statfs(&self, _ino: InodeNo) -> Result { + const FREE_BLOCKS: u64 = u64::MAX / 2; + const FREE_INODES: u64 = u64::MAX / 2; + // According to https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html any S3 object name can be at most + // 1024 bytes. + const MAX_NAME_LENGTH: u32 = 1024; + + let reply = StatFs { + free_blocks: FREE_BLOCKS, + available_blocks: FREE_BLOCKS, + free_inodes: FREE_INODES, + maximum_name_length: MAX_NAME_LENGTH, + total_blocks: FREE_BLOCKS, + ..Default::default() + }; + Ok(reply) + } } #[cfg(test)] diff --git a/mountpoint-s3/src/fuse.rs b/mountpoint-s3/src/fuse.rs index f89d04d89..3cf539e12 100644 --- a/mountpoint-s3/src/fuse.rs +++ b/mountpoint-s3/src/fuse.rs @@ -14,7 +14,7 @@ use crate::prefetch::Prefetch; use fuser::ReplyXTimes; use fuser::{ Filesystem, KernelConfig, ReplyAttr, ReplyBmap, ReplyCreate, ReplyData, ReplyEmpty, ReplyEntry, ReplyIoctl, - ReplyLock, ReplyLseek, ReplyOpen, ReplyWrite, ReplyXattr, Request, TimeOrNow, + ReplyLock, ReplyLseek, ReplyOpen, ReplyStatfs, ReplyWrite, ReplyXattr, Request, TimeOrNow, }; pub mod session; @@ -580,4 +580,21 @@ where fn getxtimes(&self, _req: &Request<'_>, ino: u64, reply: ReplyXTimes) { fuse_unsupported!("getxtimes", reply); } + + #[instrument(level="warn", skip_all, fields(req=_req.unique(), ino=ino))] + fn statfs(&self, _req: &Request<'_>, ino: u64, reply: ReplyStatfs) { + match block_on(self.fs.statfs(ino).in_current_span()) { + Ok(statfs) => reply.statfs( + statfs.total_blocks, + statfs.free_blocks, + statfs.available_blocks, + statfs.total_inodes, + statfs.free_inodes, + statfs.block_size, + statfs.maximum_name_length, + statfs.fragment_size, + ), + Err(e) => fuse_error!("statfs", reply, e), + } + } } From 1a5b59cc8a3d512894f588902ecb4e9da1c90bc3 Mon Sep 17 00:00:00 2001 From: Christian Hagemeier Date: Wed, 13 Nov 2024 13:17:43 +0000 Subject: [PATCH 2/5] Update Changelog for statfs Signed-off-by: Christian Hagemeier --- mountpoint-s3/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/mountpoint-s3/CHANGELOG.md b/mountpoint-s3/CHANGELOG.md index 84612b69c..c8d5c8797 100644 --- a/mountpoint-s3/CHANGELOG.md +++ b/mountpoint-s3/CHANGELOG.md @@ -7,6 +7,7 @@ ### Other changes * Fix an issue where `fstat` would fail and return `ESTALE` when invoked on a file descriptor after a successful `fsync`. ([#1085](https://github.com/awslabs/mountpoint-s3/pull/1085)) +* Implement statfs to report non-zero value for total blocks, free blocks, free inodes and maximum file name length (before 0 was reported ([#1118](https://github.com/awslabs/mountpoint-s3/pull/1118))). ## v1.10.0 (October 15, 2024) From e0b9c73ff03126d68b9832457ab6ebabf956892b Mon Sep 17 00:00:00 2001 From: Christian Hagemeier Date: Mon, 18 Nov 2024 10:12:14 +0000 Subject: [PATCH 3/5] Add tests + address reviewer feedback Signed-off-by: Christian Hagemeier --- mountpoint-s3/CHANGELOG.md | 2 +- mountpoint-s3/src/fs.rs | 10 +-- mountpoint-s3/tests/fuse_tests/mod.rs | 1 + mountpoint-s3/tests/fuse_tests/statfs_test.rs | 82 +++++++++++++++++++ 4 files changed, 88 insertions(+), 7 deletions(-) create mode 100644 mountpoint-s3/tests/fuse_tests/statfs_test.rs diff --git a/mountpoint-s3/CHANGELOG.md b/mountpoint-s3/CHANGELOG.md index c8d5c8797..3bb2f8030 100644 --- a/mountpoint-s3/CHANGELOG.md +++ b/mountpoint-s3/CHANGELOG.md @@ -6,8 +6,8 @@ ### Other changes +* Implement statfs to report non-zero synthetic values. This may unblock applications which rely on verifying there is available space before creating new files.([#1118](https://github.com/awslabs/mountpoint-s3/pull/1118)). * Fix an issue where `fstat` would fail and return `ESTALE` when invoked on a file descriptor after a successful `fsync`. ([#1085](https://github.com/awslabs/mountpoint-s3/pull/1085)) -* Implement statfs to report non-zero value for total blocks, free blocks, free inodes and maximum file name length (before 0 was reported ([#1118](https://github.com/awslabs/mountpoint-s3/pull/1118))). ## v1.10.0 (October 15, 2024) diff --git a/mountpoint-s3/src/fs.rs b/mountpoint-s3/src/fs.rs index 834772339..c8e207756 100644 --- a/mountpoint-s3/src/fs.rs +++ b/mountpoint-s3/src/fs.rs @@ -130,6 +130,7 @@ pub struct StatFs { impl Default for StatFs { fn default() -> Self { + // Default values copied from Fuser (https://github.com/cberner/fuser/blob/e18bd9bf9071ecd8be62993726e06ff11d6ec709/src/lib.rs#L695-L698) Self { total_blocks: 0, free_blocks: 0, @@ -827,18 +828,15 @@ where } pub async fn statfs(&self, _ino: InodeNo) -> Result { - const FREE_BLOCKS: u64 = u64::MAX / 2; - const FREE_INODES: u64 = u64::MAX / 2; - // According to https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html any S3 object name can be at most - // 1024 bytes. - const MAX_NAME_LENGTH: u32 = 1024; + const FREE_BLOCKS: u64 = u64::MAX / 1024; + const FREE_INODES: u64 = u64::MAX / 1024; let reply = StatFs { free_blocks: FREE_BLOCKS, available_blocks: FREE_BLOCKS, free_inodes: FREE_INODES, - maximum_name_length: MAX_NAME_LENGTH, total_blocks: FREE_BLOCKS, + total_inodes: FREE_INODES, ..Default::default() }; Ok(reply) diff --git a/mountpoint-s3/tests/fuse_tests/mod.rs b/mountpoint-s3/tests/fuse_tests/mod.rs index 2b24c0d1e..72ff962fc 100644 --- a/mountpoint-s3/tests/fuse_tests/mod.rs +++ b/mountpoint-s3/tests/fuse_tests/mod.rs @@ -11,3 +11,4 @@ mod semantics_doc_test; mod setattr_test; mod unlink_test; mod write_test; +mod statfs_test; diff --git a/mountpoint-s3/tests/fuse_tests/statfs_test.rs b/mountpoint-s3/tests/fuse_tests/statfs_test.rs new file mode 100644 index 000000000..c7f41692c --- /dev/null +++ b/mountpoint-s3/tests/fuse_tests/statfs_test.rs @@ -0,0 +1,82 @@ +use crate::common::fuse::{self, TestSessionCreator}; +use test_case::test_case; + +/// Tests that non-zero empty space is reported +fn statfs_test_available_nonzero(creator_fn: impl TestSessionCreator, prefix: &str) { + let test_session = creator_fn(prefix, Default::default()); + let mount_dir = test_session.mount_dir; + let stats = nix::sys::statvfs::statvfs(&mount_dir.into_path()).unwrap(); + assert_ne!(stats.blocks_free(), 0); + assert_ne!(stats.blocks_available(), 0); + assert_ne!(stats.blocks(), 0); +} + +/// Tests that default values from FUSER are reported for mpst fields +fn statfs_test_fuser_defaults(creator_fn: impl TestSessionCreator, prefix: &str) { + let test_session = creator_fn(prefix, Default::default()); + let mount_dir = test_session.mount_dir; + let stats = nix::sys::statvfs::statvfs(&mount_dir.into_path()).unwrap(); + //assert_eq!(stats.name_max(), 255); + // These five aren't default values but set by us, so maybe drop + assert_eq!(stats.blocks(), u64::MAX / 1024); + assert_eq!(stats.blocks_free(), u64::MAX / 1024); + assert_eq!(stats.blocks_available(), u64::MAX / 1024); + assert_eq!(stats.files(), u64::MAX / 1024); + assert_eq!(stats.files_available(), u64::MAX / 1024); + // These are default values from the Default implementation + assert_eq!(stats.block_size(), 512); + assert_eq!(stats.name_max(), 255); + println!("{}", stats.fragment_size().to_string()); + // This may be a bit surprising, however as we set fsize to 0, + // it will be automatically set to the block_size, if it is not available + // c.f. https://stackoverflow.com/questions/54823541/what-do-f-bsize-and-f-frsize-in-struct-statvfs-stand-for + assert_eq!(stats.fragment_size(), 255); +} + +/// Test that total blocks >= blocks_free, +/// as some tools rely on calculations with these values to determine percentage of blocks available +fn statfs_test_block_arithmetic(creator_fn: impl TestSessionCreator, prefix: &str) { + let test_session = creator_fn(prefix, Default::default()); + let mount_dir = test_session.mount_dir; + let stats = nix::sys::statvfs::statvfs(&mount_dir.into_path()).unwrap(); + assert!(stats.blocks() >= stats.blocks_available()); +} + +#[test_case(""; "no prefix")] +#[test_case("statfs_report_nonzero_test"; "prefix")] +fn statfs_report_nonzero_test_mock(prefix: &str) { + statfs_test_available_nonzero(fuse::mock_session::new, prefix); +} + +#[cfg(feature = "s3_tests")] +#[test_case(""; "no prefix")] +#[test_case("statfs_report_nonzero_test"; "prefix")] +fn statfs_report_nonzero_s3(prefix: &str) { + statfs_test_available_nonzero(fuse::s3_session::new, prefix); +} + +#[test_case(""; "no prefix")] +#[test_case("statfs_report_fuser_defaults_test"; "prefix")] +fn statfs_report_fuser_defaults_mock(prefix: &str) { + statfs_test_fuser_defaults(fuse::mock_session::new, prefix); +} + +#[cfg(feature = "s3_tests")] +#[test_case(""; "no prefix")] +#[test_case("statfs_report_nonzero_test"; "prefix")] +fn statfs_report_fuser_defaults_s3(prefix: &str) { + statfs_test_available_nonzero(fuse::s3_session::new, prefix); +} + +#[test_case(""; "no prefix")] +#[test_case("statfs_block_arithmetic_test"; "prefix")] +fn statfs_block_arithmetic_mock(prefix: &str) { + statfs_test_block_arithmetic(fuse::mock_session::new, prefix); +} + +#[cfg(feature = "s3_tests")] +#[test_case(""; "no prefix")] +#[test_case("statfs_block_arithmetic_test"; "prefix")] +fn statfs_block_arithmetic_s3(prefix: &str) { + statfs_test_block_arithmetic(fuse::s3_session::new, prefix); +} From 83d94bbb6d08d89cc53772474a92098ba4827a2b Mon Sep 17 00:00:00 2001 From: Christian Hagemeier Date: Mon, 18 Nov 2024 10:29:39 +0000 Subject: [PATCH 4/5] Fix tests Signed-off-by: Christian Hagemeier --- mountpoint-s3/tests/fuse_tests/mod.rs | 2 +- mountpoint-s3/tests/fuse_tests/statfs_test.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mountpoint-s3/tests/fuse_tests/mod.rs b/mountpoint-s3/tests/fuse_tests/mod.rs index 72ff962fc..8817620ea 100644 --- a/mountpoint-s3/tests/fuse_tests/mod.rs +++ b/mountpoint-s3/tests/fuse_tests/mod.rs @@ -9,6 +9,6 @@ mod readdir_test; mod rmdir_test; mod semantics_doc_test; mod setattr_test; +mod statfs_test; mod unlink_test; mod write_test; -mod statfs_test; diff --git a/mountpoint-s3/tests/fuse_tests/statfs_test.rs b/mountpoint-s3/tests/fuse_tests/statfs_test.rs index c7f41692c..34f05b4f0 100644 --- a/mountpoint-s3/tests/fuse_tests/statfs_test.rs +++ b/mountpoint-s3/tests/fuse_tests/statfs_test.rs @@ -30,7 +30,7 @@ fn statfs_test_fuser_defaults(creator_fn: impl TestSessionCreator, prefix: &str) // This may be a bit surprising, however as we set fsize to 0, // it will be automatically set to the block_size, if it is not available // c.f. https://stackoverflow.com/questions/54823541/what-do-f-bsize-and-f-frsize-in-struct-statvfs-stand-for - assert_eq!(stats.fragment_size(), 255); + assert_eq!(stats.fragment_size(), 512); } /// Test that total blocks >= blocks_free, From 4e2e2b4376d0a7e42208dcef199350754a7a52cf Mon Sep 17 00:00:00 2001 From: Christian Hagemeier Date: Mon, 18 Nov 2024 10:58:46 +0000 Subject: [PATCH 5/5] Fix tests after merge Signed-off-by: Christian Hagemeier --- mountpoint-s3/tests/fuse_tests/statfs_test.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mountpoint-s3/tests/fuse_tests/statfs_test.rs b/mountpoint-s3/tests/fuse_tests/statfs_test.rs index 34f05b4f0..838db3251 100644 --- a/mountpoint-s3/tests/fuse_tests/statfs_test.rs +++ b/mountpoint-s3/tests/fuse_tests/statfs_test.rs @@ -4,8 +4,8 @@ use test_case::test_case; /// Tests that non-zero empty space is reported fn statfs_test_available_nonzero(creator_fn: impl TestSessionCreator, prefix: &str) { let test_session = creator_fn(prefix, Default::default()); - let mount_dir = test_session.mount_dir; - let stats = nix::sys::statvfs::statvfs(&mount_dir.into_path()).unwrap(); + let mount_dir = test_session.mount_path(); + let stats = nix::sys::statvfs::statvfs(mount_dir.into()).unwrap(); assert_ne!(stats.blocks_free(), 0); assert_ne!(stats.blocks_available(), 0); assert_ne!(stats.blocks(), 0); @@ -14,8 +14,8 @@ fn statfs_test_available_nonzero(creator_fn: impl TestSessionCreator, prefix: &s /// Tests that default values from FUSER are reported for mpst fields fn statfs_test_fuser_defaults(creator_fn: impl TestSessionCreator, prefix: &str) { let test_session = creator_fn(prefix, Default::default()); - let mount_dir = test_session.mount_dir; - let stats = nix::sys::statvfs::statvfs(&mount_dir.into_path()).unwrap(); + let mount_dir = test_session.mount_path(); + let stats = nix::sys::statvfs::statvfs(mount_dir.into()).unwrap(); //assert_eq!(stats.name_max(), 255); // These five aren't default values but set by us, so maybe drop assert_eq!(stats.blocks(), u64::MAX / 1024); @@ -37,8 +37,8 @@ fn statfs_test_fuser_defaults(creator_fn: impl TestSessionCreator, prefix: &str) /// as some tools rely on calculations with these values to determine percentage of blocks available fn statfs_test_block_arithmetic(creator_fn: impl TestSessionCreator, prefix: &str) { let test_session = creator_fn(prefix, Default::default()); - let mount_dir = test_session.mount_dir; - let stats = nix::sys::statvfs::statvfs(&mount_dir.into_path()).unwrap(); + let mount_dir = test_session.mount_path(); + let stats = nix::sys::statvfs::statvfs(mount_dir.into()).unwrap(); assert!(stats.blocks() >= stats.blocks_available()); }