Skip to content

Commit

Permalink
Implement statfs with synthetic values (#1118)
Browse files Browse the repository at this point in the history
## Description of change
This PR adds support for calling `statfs` on virtual file system created
using mountpoint.
Some applications depend on the filesystem reporting non-zero available
space; currently mountpoint reports 0 as number of available blocks,
which can cause these applications to not work as expected.

This PR (building on #871) implements statfs with synthetic values
(4611686018427387904 free blocks).
For example, the DF output now is: 
```
mountpoint-s3  4611686018427387904        0 4611686018427387904   0% /local/home/chagem/mnt/bucket
```
Thus, checks for available space should no longer fail. 


Relevant issues: #710.  


### Does this change impact existing behavior?

This change impacts existing behaviour, as Mountpoint will report
non-zero value for total blocks, free blocks, free inodes and maximum
file name length.

### Does this change need a changelog entry?

Yes, addressed.

---

By submitting this pull request, I confirm that my contribution is made
under the terms of the Apache 2.0 license and I agree to the terms of
the [Developer Certificate of Origin
(DCO)](https://developercertificate.org/).

---------

Signed-off-by: Christian Hagemeier <chagem@amazon.com>
  • Loading branch information
c-hagem authored Nov 21, 2024
1 parent ff191c1 commit 630e5a0
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 1 deletion.
4 changes: 4 additions & 0 deletions mountpoint-s3/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## Unreleased

### 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)).

## v1.11.0 (November 21, 2024)

### New features
Expand Down
52 changes: 52 additions & 0 deletions mountpoint-s3/src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,43 @@ pub struct DirectoryEntry {
lookup: LookedUp,
}

/// Reply to a 'statfs' call
#[derive(Debug)]
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 {
// Default values copied from Fuser (https://github.com/cberner/fuser/blob/e18bd9bf9071ecd8be62993726e06ff11d6ec709/src/lib.rs#L695-L698)
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<Client, Prefetcher> S3Filesystem<Client, Prefetcher>
where
Client: ObjectClient + Clone + Send + Sync + 'static,
Expand Down Expand Up @@ -840,6 +877,21 @@ where
}
Ok(self.superblock.unlink(&self.client, parent_ino, name).await?)
}

pub async fn statfs(&self, _ino: InodeNo) -> Result<StatFs, Error> {
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,
total_blocks: FREE_BLOCKS,
total_inodes: FREE_INODES,
..Default::default()
};
Ok(reply)
}
}

#[cfg(test)]
Expand Down
19 changes: 18 additions & 1 deletion mountpoint-s3/src/fuse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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),
}
}
}
1 change: 1 addition & 0 deletions mountpoint-s3/tests/fuse_tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ mod readdir_test;
mod rmdir_test;
mod semantics_doc_test;
mod setattr_test;
mod statfs_test;
mod unlink_test;
mod write_test;
58 changes: 58 additions & 0 deletions mountpoint-s3/tests/fuse_tests/statfs_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use crate::common::fuse::{self, TestSessionCreator};
use test_case::test_case;

/// Tests that static values we set are reported correctly.
fn statfs_test_static_values(creator_fn: impl TestSessionCreator, prefix: &str) {
let test_session = creator_fn(prefix, Default::default());
let mount_dir = test_session.mount_path();
let stats = nix::sys::statvfs::statvfs(mount_dir).unwrap();
// Asserts that we report free space, we don't care about actual values here
assert_ne!(stats.blocks(), 0);
assert_ne!(stats.blocks_free(), 0);
assert_ne!(stats.blocks_available(), 0);
// These two are values set by us
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);
// 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(), 512);
}

/// 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_path();
let stats = nix::sys::statvfs::statvfs(mount_dir).unwrap();
assert!(stats.blocks() >= stats.blocks_available());
}

#[test_case(""; "no prefix")]
#[test_case("statfs_static_values_test"; "prefix")]
fn statfs_report_static_values_mock(prefix: &str) {
statfs_test_static_values(fuse::mock_session::new, prefix);
}

#[cfg(feature = "s3_tests")]
#[test_case(""; "no prefix")]
#[test_case("statfs_static_values_test"; "prefix")]
fn statfs_report_static_values_s3(prefix: &str) {
statfs_test_static_values(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);
}

0 comments on commit 630e5a0

Please sign in to comment.