diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index cd389c722195cf939bee985e26df2de82caf50f2..67a6480144d3ccebbe39694d528956e96300b3f8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -62,6 +62,9 @@ gkfs: - sed -i 's/constexpr bool use_dentry_cache = false;/constexpr bool use_dentry_cache = true;/g' "${CI_PROJECT_DIR}/include/config.hpp" #- sed -i 's/constexpr auto zero_buffer_before_read = false;/constexpr auto zero_buffer_before_read = true;/g' "${CI_PROJECT_DIR}/include/config.hpp" #- sed -i 's/constexpr auto implicit_data_removal = true;/constexpr auto implicit_data_removal = false;/g' "${CI_PROJECT_DIR}/include/config.hpp" + # install libfuse + - apt-get update + - apt-get install -y libfuse3-dev fuse3 # use ccache - ccache --zero-stats -M 750MiB -F 800 --evict-older-than 10d - /usr/sbin/update-ccache-symlinks @@ -156,7 +159,7 @@ gkfs:integration: needs: ['gkfs'] # we need to remove gkfs dependencies on manual parallel: matrix: - - SUBTEST: [ data, status, syscalls, directories, operations, position, shell, rename ] + - SUBTEST: [ data, status, syscalls, directories, operations, position, shell, rename, fuse ] rules: - if: '$CI_MERGE_REQUEST_EVENT_TYPE == "detached"' when: never @@ -533,7 +536,7 @@ cppcheck: script: - cd ${CI_PROJECT_DIR} - rm -rf external gkfs - - cppcheck -I/usr/local/include --xml --enable=warning,style,performance --force ${CI_PROJECT_DIR} 2> cppcheck_out.xml + - cppcheck -I/usr/local/include --xml --enable=warning,style,performance --force ${CI_PROJECT_DIR} 2> cppcheck_out.xml - cppcheck-codequality --input-file cppcheck_out.xml --output-file cppcheck.json #change paths after_script: @@ -544,7 +547,7 @@ cppcheck: reports: codequality: cppcheck2.json expire_in: 5 days - + ################################################################################ ## Deployment of documentation and reports diff --git a/CMake/gkfs-options.cmake b/CMake/gkfs-options.cmake index 8a03d7c48ab985ae55c4ff8ce9d82c1837376f61..b70b99e0761c802e4bc9013d13125e06daea6348 100644 --- a/CMake/gkfs-options.cmake +++ b/CMake/gkfs-options.cmake @@ -248,6 +248,13 @@ gkfs_define_option( DEFAULT_VALUE ON ) +# use old resolve function +gkfs_define_option( + GKFS_BUILD_FUSE + HELP_TEXT "Build FUSE client" + DEFAULT_VALUE ON +) + # use old resolve function gkfs_define_option( GKFS_USE_LEGACY_PATH_RESOLVE diff --git a/CMakeLists.txt b/CMakeLists.txt index 98fc0776b593c1111b6dbe95213c26121f06f4d4..d930172e0b0d8d16fee987613a4948aa98dd10bf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -216,6 +216,11 @@ find_package(Threads REQUIRED) # details transparently find_package(Filesystem REQUIRED) +find_package(PkgConfig REQUIRED) +pkg_check_modules(FUSE3 REQUIRED fuse3) +include_directories(${FUSE3_INCLUDE_DIRS}) +link_directories(${FUSE3_LIBRARY_DIRS}) +add_definitions(${FUSE3_CFLAGS_OTHER}) # Search for 'source-only' dependencies ############################################################################### diff --git a/include/client/fuse/fuse_client.hpp b/include/client/fuse/fuse_client.hpp new file mode 100644 index 0000000000000000000000000000000000000000..7c9e255a1262e60a97641e84d3f9d66b0f30850e --- /dev/null +++ b/include/client/fuse/fuse_client.hpp @@ -0,0 +1,125 @@ +/* + Copyright 2018-2025, Barcelona Supercomputing Center (BSC), Spain + Copyright 2015-2025, Johannes Gutenberg Universitaet Mainz, Germany + + This software was partially supported by the + EC H2020 funded project NEXTGenIO (Project ID: 671951, www.nextgenio.eu). + + This software was partially supported by the + ADA-FS project under the SPPEXA project funded by the DFG. + + This software was partially supported by the + the European Union’s Horizon 2020 JTI-EuroHPC research and + innovation programme, by the project ADMIRE (Project ID: 956748, + admire-eurohpc.eu) + + This project was partially promoted by the Ministry for Digital Transformation + and the Civil Service, within the framework of the Recovery, + Transformation and Resilience Plan - Funded by the European Union + -NextGenerationEU. + + This file is part of GekkoFS' POSIX interface. + + GekkoFS' POSIX interface is free software: you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation, either version 3 of the License, + or (at your option) any later version. + + GekkoFS' POSIX interface is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with GekkoFS' POSIX interface. If not, see + . + + SPDX-License-Identifier: LGPL-3.0-or-later +*/ + +#ifndef GKFS_CLIENT_FUSE_CONTEXT_HPP +#define GKFS_CLIENT_FUSE_CONTEXT_HPP + +#include "fuse_log.h" +#include +extern "C" { +#define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 12) +#include +} + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __FreeBSD__ +#include +#include +#endif + +// GekkoFS Project Headers +#include +#include +#include // Assumed to provide LOG and CTX +#include +#include +#include +#include +#include + +// TODO do we really need the stat here? no i dont think so +struct Inode { + std::string path; + struct stat st; + uint64_t lookup_count; +}; + +enum { + CACHE_NEVER, + CACHE_NORMAL, + CACHE_ALWAYS, +}; + +struct u_data { + pthread_mutex_t mutex; + int debug; + int writeback; + int direct_io; + int max_readahead; + int fifo; + int access; + int xattr; + char* mountpoint; + double timeout; + int cache; + int timeout_set; +}; + +struct GkfsDir { // Hypothetical structure that might be used if DIR is cast + int fd; + long int tell_pos; // for telldir/seekdir + char* path; + // other members libc DIR might have +}; + +#endif // GKFS_CLIENT_FUSE_CONTEXT_HPP diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt index a7d0247f5f6065f16651f2d2cf7affee9d017233..ed6b57004551e7ac38056e67292cdb3ed4295642 100644 --- a/src/client/CMakeLists.txt +++ b/src/client/CMakeLists.txt @@ -37,7 +37,7 @@ add_library(gkfs_common SHARED) target_sources(gkfs_common PUBLIC path.cpp - PRIVATE + PRIVATE logging.cpp open_file_map.cpp open_dir.cpp @@ -104,10 +104,30 @@ target_sources( hooks.cpp preload.cpp preload_context.cpp - preload_util.cpp + preload_util.cpp ) endif () +if (GKFS_BUILD_FUSE) +add_executable(fuse_client "") +target_sources(fuse_client + PUBLIC + ${CMAKE_CURRENT_LIST_DIR}/fuse/fuse_client.cpp + PRIVATE + ${INCLUDE_DIR}/client/fuse/fuse_client.hpp + ) + +target_link_libraries(fuse_client + PRIVATE gkfs_common metadata distributor env_util arithmetic path_util rpc_utils + ${FUSE3_LIBRARIES} + gkfs_user_lib + ) + +target_include_directories(fuse_client + PRIVATE + ${FUSE3_INCLUDE_DIRS} + ) +endif() if (GKFS_BUILD_USER_LIB) target_compile_definitions(gkfs_user_lib PUBLIC BYPASS_SYSCALL ENABLE_USER) @@ -217,9 +237,13 @@ install( ) endif () +if (GKFS_BUILD_FUSE) +install(TARGETS fuse_client RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) +endif () + install( TARGETS gkfs_common LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/gkfs -) \ No newline at end of file +) diff --git a/src/client/fuse/fuse_client.cpp b/src/client/fuse/fuse_client.cpp new file mode 100644 index 0000000000000000000000000000000000000000..b9a1197997f4021f2902086b87b39e80a203afc4 --- /dev/null +++ b/src/client/fuse/fuse_client.cpp @@ -0,0 +1,1118 @@ +/* + Copyright 2018-2025, Barcelona Supercomputing Center (BSC), Spain + Copyright 2015-2025, Johannes Gutenberg Universitaet Mainz, Germany + + This software was partially supported by the + EC H2020 funded project NEXTGenIO (Project ID: 671951, www.nextgenio.eu). + + This software was partially supported by the + ADA-FS project under the SPPEXA project funded by the DFG. + + This software was partially supported by the + the European Union’s Horizon 2020 JTI-EuroHPC research and + innovation programme, by the project ADMIRE (Project ID: 956748, + admire-eurohpc.eu) + + This project was partially promoted by the Ministry for Digital Transformation + and the Civil Service, within the framework of the Recovery, + Transformation and Resilience Plan - Funded by the European Union + -NextGenerationEU. + + This file is part of GekkoFS' POSIX interface. + + GekkoFS' POSIX interface is free software: you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation, either version 3 of the License, + or (at your option) any later version. + + GekkoFS' POSIX interface is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with GekkoFS' POSIX interface. If not, see + . + + SPDX-License-Identifier: LGPL-3.0-or-later +*/ + +#include + +static struct fuse_lowlevel_ops ll_ops; +static std::mutex ino_mutex; +static std::unordered_map ino_map; +static std::unordered_map path_map; +static std::unordered_map local_fifos; +static fuse_ino_t next_ino = 2; // reserve 1 for root +static const std::string fifo_path = "/tmp/gekkofs_fifos/"; + +#ifdef GKFS_DEBUG_BUILD +#define DEBUG_INFO(ud, fmt, ...) \ + do { \ + if((ud) && (ud)->debug) { \ + fuse_log(FUSE_LOG_DEBUG, "[DEBUG] " fmt "\n", ##__VA_ARGS__); \ + } \ + } while(0) +#else +#define DEBUG_INFO(...) /* No debug output */ +#endif + +static fuse_ino_t +alloc_inode(const std::string& path) { + std::lock_guard lk(ino_mutex); + fuse_ino_t ino = next_ino++; + ino_map[ino] = {path, {}, 1}; + path_map[path] = ino; + return ino; +} + +static Inode* +get_inode(fuse_ino_t ino) { + std::lock_guard lk(ino_mutex); + auto it = ino_map.find(ino); + return it != ino_map.end() ? &it->second : nullptr; +} + +static struct u_data* +udata(fuse_req_t req) { + return (struct u_data*) fuse_req_userdata(req); +} + +static std::string +get_path(const Inode* inode, const char* name) { + if(name[0] == '/') { + return std::string(name); + } + if(inode->path == "/") { + return inode->path + name; + } else { + return inode->path + "/" + name; + } +} + +static const struct fuse_opt lo_opts[] = { + {"writeback", offsetof(struct u_data, writeback), 1}, + {"no_writeback", offsetof(struct u_data, writeback), 0}, + {"direct_io", offsetof(struct u_data, direct_io), 1}, + {"no_direct_io", offsetof(struct u_data, direct_io), 0}, + {"max_readahead=%ui", offsetof(struct u_data, max_readahead), 0}, + {"fifo", offsetof(struct u_data, fifo), 1}, + {"no_fifo", offsetof(struct u_data, fifo), 0}, + {"access", offsetof(struct u_data, access), 1}, + {"no_access", offsetof(struct u_data, access), 0}, + {"xattr", offsetof(struct u_data, xattr), 1}, + {"no_xattr", offsetof(struct u_data, xattr), 0}, + {"timeout=%lf", offsetof(struct u_data, timeout), 0}, + {"timeout=", offsetof(struct u_data, timeout_set), 1}, + {"cache=never", offsetof(struct u_data, cache), CACHE_NEVER}, + {"cache=auto", offsetof(struct u_data, cache), CACHE_NORMAL}, + {"cache=always", offsetof(struct u_data, cache), CACHE_ALWAYS}, + + FUSE_OPT_END}; + +static void +passthrough_ll_help(void) { + printf(" -o writeback Enable writeback\n" + " -o no_writeback Disable write back\n" + " -o direct_io Enables direct io\n" + " -o no_direct_io Disable direct io\n" + " -o max_readahead=1 Amount of allowed readaheads\n" + " -o fifo Enable fifo, disables GekkoFS access check\n" + " -o no_fifo Disable fifo\n" + " -o access Enable GekkoFS access check if fifo is deactivated\n" + " -o no_access Disable access handler\n" + " -o xattr Enable xattr\n" + " -o no_xattr Disable xattr\n" + " -o timeout=1.0 Caching timeout\n" + " -o timeout=0/1 Timeout is set\n" + " -o cache=never Disable cache\n" + " -o cache=auto Auto enable cache\n" + " -o cache=always Cache always\n"); +} + +static void +init_handler(void* userdata, struct fuse_conn_info* conn) { + struct u_data* ud = (struct u_data*) userdata; + DEBUG_INFO(ud, "init handler readahead %i direct_io %i", ud->max_readahead, + ud->direct_io); + + // TODO check other capabilities e.g. FUSE_CAP_READDIRPLUS + if(ud->writeback) { +#if FUSE_MAJOR_VERSION > 3 || \ + (FUSE_MAJOR_VERSION == 3 && FUSE_MINOR_VERSION >= 12) + fuse_set_feature_flag(conn, FUSE_CAP_WRITEBACK_CACHE); +#else + // for older fuse versions like on the ubuntu22 + conn->want |= FUSE_CAP_WRITEBACK_CACHE; +#endif + DEBUG_INFO(ud, "init_handler: try to activate writeback", + ud->max_readahead, ud->direct_io); + } + // if(lo->flock && conn->capable & FUSE_CAP_FLOCK_LOCKS) { + // has_flag = fuse_set_feature_flag(conn, FUSE_CAP_FLOCK_LOCKS); + // if(lo->debug && has_flag) + // fuse_log(FUSE_LOG_DEBUG, "init_handler: activating flock + // locks\n"); + // } + + /* Disable the receiving and processing of FUSE_INTERRUPT requests */ + // conn->no_interrupt = 1; + conn->max_readahead = ud->max_readahead; +} + +static void +destroy_handler(void* userdata) { + struct u_data* ud = (struct u_data*) userdata; + DEBUG_INFO(ud, "destroy handler"); + // userdata is GekkoFuse* if passed +} + +static void +lookup_handler(fuse_req_t req, fuse_ino_t parent, const char* name) { + auto* ud = udata(req); + DEBUG_INFO(ud, "lookup handler ino %u", parent); + auto* parent_inode = get_inode(parent); + if(!parent_inode) { + fuse_reply_err(req, ENOENT); + return; + } + std::string child = get_path(parent_inode, name); + DEBUG_INFO(ud, "lookup %s", child.c_str()); + + if(ud->fifo) { + auto iit = local_fifos.find(child); + if(iit != local_fifos.end()) { + const struct stat& st = iit->second; + + fuse_entry_param e{}; + e.ino = st.st_ino; + e.attr = st; + e.attr_timeout = ud->timeout; + e.entry_timeout = ud->timeout; + + fuse_reply_entry(req, &e); + return; + } + } + + // See if we already have this path + auto it = path_map.find(child); + fuse_ino_t ino; + if(it != path_map.end()) { + ino = it->second; + ino_map[ino].lookup_count++; + } else { + ino = alloc_inode(child); + } + + struct stat st; + int rc = gkfs::syscall::gkfs_stat(child, &st); + if(rc < 0) { + fuse_reply_err(req, ENOENT); + return; + } + ino_map[ino].st = st; + fuse_entry_param e = {}; + e.ino = ino; + e.attr = st; + e.attr_timeout = ud->timeout; + e.entry_timeout = ud->timeout; + fuse_reply_entry(req, &e); +} + +static void +getattr_handler(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) { + auto* ud = udata(req); + DEBUG_INFO(ud, "getattr handler"); + auto* inode = get_inode(ino); + if(!inode) { + fuse_reply_err(req, ENOENT); + return; + } + // query GekkoFS for latest attr + struct stat st; + int rc = gkfs::syscall::gkfs_stat(inode->path, &st); + if(rc) { + DEBUG_INFO(ud, "getattr error %u", rc); + fuse_reply_err(req, errno); + return; + } + inode->st = st; + fuse_reply_attr(req, &st, ud->timeout); +} + +static void +setattr_handler(fuse_req_t req, fuse_ino_t ino, struct stat* attr, int to_set, + struct fuse_file_info* fi) { + auto* ud = udata(req); + DEBUG_INFO(ud, "setattr handler ino %u", ino); + auto* inode = get_inode(ino); + if(!inode) { + fuse_reply_err(req, ENOENT); + return; + } + + if(to_set & FUSE_SET_ATTR_SIZE) { + off_t new_size = attr->st_size; + int res = gkfs::syscall::gkfs_truncate(inode->path, new_size); + if(res < 0) { + DEBUG_INFO(ud, "setattr truncate failed on %s", + inode->path.c_str()); + fuse_reply_err(req, EIO); + return; + } + // Update cached stat so users see the new size + inode->st.st_size = new_size; + } + + if(to_set & FUSE_SET_ATTR_ATIME) + inode->st.st_atim = attr->st_atim; + if(to_set & FUSE_SET_ATTR_MTIME) + inode->st.st_mtim = attr->st_mtim; + + // TODO because we cannot save the attributes in gekko, we just return the + // buffered results of stat + fuse_reply_attr(req, &inode->st, ud->timeout); + return; +} + +static void +open_handler(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) { + auto* ud = udata(req); + DEBUG_INFO(ud, "open handler"); + auto* inode = get_inode(ino); + if(!inode) { + fuse_reply_err(req, ENOENT); + return; + } + const int mode = 0644; // -rw-r--r-- I think that doesnt matter anyway + int fd = gkfs::syscall::gkfs_open(inode->path, mode, + fi->flags); // TODO mode! + if(fd < 0) { + fuse_reply_err(req, ENOENT); + return; + } + fi->fh = fd; // TODO file handle == file descriptor? + fi->direct_io = ud->direct_io; + fuse_reply_open(req, fi); +} + +static void +lseek_handler(fuse_req_t req, fuse_ino_t ino, off_t off, int whence, + struct fuse_file_info* fi) { + auto* ud = udata(req); + DEBUG_INFO(ud, "lseek handler"); + int lc = gkfs::syscall::gkfs_lseek(fi->fh, off, whence); + if(lc < 0) { + fuse_reply_err(req, 1); + return; + } + fuse_reply_lseek(req, lc); +} + +static void +read_handler(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off, + struct fuse_file_info* fi) { + auto* ud = udata(req); + DEBUG_INFO(ud, "read handler"); + auto* inode = get_inode(ino); + if(!inode) { + fuse_reply_err(req, ENOENT); + return; + } + std::vector buf(size); + int rc = gkfs::syscall::gkfs_pread(fi->fh, buf.data(), size, off); + if(rc < 0) { + DEBUG_INFO(ud, "read fail"); + fuse_reply_err(req, errno); + return; + } + fuse_reply_buf(req, buf.data(), rc); +} + +static void +write_handler(fuse_req_t req, fuse_ino_t ino, const char* buf, size_t size, + off_t off, struct fuse_file_info* fi) { + auto* ud = udata(req); + DEBUG_INFO(ud, "write handler"); + auto* inode = get_inode(ino); + if(!inode) { + fuse_reply_err(req, ENOENT); + return; + } + int rc = gkfs::syscall::gkfs_pwrite(fi->fh, buf, size, off); + if(rc < 0) { + DEBUG_INFO(ud, "write fail"); + fuse_reply_err(req, errno); + return; + } + fuse_reply_write(req, rc); +} + +static void +create_handler(fuse_req_t req, fuse_ino_t parent, const char* name, mode_t mode, + struct fuse_file_info* fi) { + auto* ud = udata(req); + auto* parent_inode = get_inode(parent); + if(!parent_inode) { + fuse_reply_err(req, ENOENT); + return; + } + std::string path = get_path(parent_inode, name); + DEBUG_INFO(ud, "create handler %s", path.c_str()); + int fd = gkfs::syscall::gkfs_open(path, mode, fi->flags | O_CREAT); + if(fd < 0) { + DEBUG_INFO(ud, "create -> open failed errno %i", errno); + fuse_reply_err(req, errno); + return; + } + fi->fh = fd; + fi->direct_io = ud->direct_io; + struct stat st; + int sc = gkfs::syscall::gkfs_stat(path, &st); + if(sc == -1) { + fuse_reply_err(req, ENOENT); + return; + } + fuse_ino_t ino = alloc_inode(path); + DEBUG_INFO(ud, "create new inode ino %i", ino); + ino_map[ino].st = st; + fuse_entry_param e = {}; + e.ino = ino; + e.attr = st; + e.attr_timeout = ud->timeout; + e.entry_timeout = ud->timeout; + fuse_reply_create(req, &e, fi); +} + +/// TODO normally, the file should only be removed if the lookup count is zero, +/// problem? +static void +unlink_handler(fuse_req_t req, fuse_ino_t parent, const char* name) { + auto* ud = udata(req); + DEBUG_INFO(ud, "unlink handler"); + auto* parent_inode = get_inode(parent); + if(!parent_inode) { + fuse_reply_err(req, ENOENT); + return; + } + std::string path = get_path(parent_inode, name); + + if(ud->fifo) { + auto it = local_fifos.find(path); + if(it != local_fifos.end()) { + std::string real = fifo_path + std::string(name); + unlink(real.c_str()); + local_fifos.erase(it); + fuse_reply_err(req, 0); + return; + } + } + + int rc = gkfs::syscall::gkfs_remove(path); + auto it_src = path_map.find(path); + if(it_src != path_map.end()) { + path_map.erase(it_src); + ino_map.erase(it_src->second); + } + if(rc == -1) { + fuse_reply_err(req, 1); + return; + } + fuse_reply_err(req, 0); +} + +static void +opendir_handler(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) { + auto* ud = udata(req); + DEBUG_INFO(ud, "opendir handler"); + auto* inode = get_inode(ino); + if(!inode) { + fuse_reply_err(req, ENOTDIR); + return; + } + DEBUG_INFO(ud, "open dir %s", inode->path.c_str()); + + const int fd = gkfs::syscall::gkfs_opendir(inode->path); + + DEBUG_INFO(ud, "\t with fd %i \n", fd); + + if(fd < 0) { + fuse_reply_err(req, ENOTDIR); + return; + } + + fi->fh = (uint64_t) fd; + fuse_reply_open(req, fi); +} + +static void +readdir_handler(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off, + struct fuse_file_info* fi) { + auto* ud = udata(req); + DEBUG_INFO(ud, "readdir handler"); + + auto open_dir = CTX->file_map()->get_dir(fi->fh); + + DEBUG_INFO(ud, "read dir %s", open_dir->path().c_str()); + + if(open_dir == nullptr) { + fuse_reply_err(req, EBADF); + return; + } + + // Allocate a buffer to accumulate entries + char* buf = static_cast(malloc(size)); + if(!buf) { + fuse_reply_err(req, ENOMEM); + return; + } + + size_t bytes_filled = 0; + size_t pos = off; + + while(pos < open_dir->size()) { + auto de = open_dir->getdent(pos); + + struct stat st{}; + st.st_ino = + std::hash()(open_dir->path() + "/" + de.name()); + st.st_mode = (de.type() == gkfs::filemap::FileType::regular) ? S_IFREG + : S_IFDIR; + + size_t entry_size = + fuse_add_direntry(req, buf + bytes_filled, size - bytes_filled, + de.name().c_str(), &st, pos + 1); + + if(entry_size > size - bytes_filled) + break; // not enough space left + + bytes_filled += entry_size; + pos += 1; + } + + if(ud->fifo) { + int i = 0; + for(auto const& kv : local_fifos) { + if(i < off) { + i++; + continue; + } + std::filesystem::path vpath = kv.first; + const struct stat& st = kv.second; + + // check if FIFO belongs to this directory + if(vpath.parent_path() != open_dir->path()) + continue; + + std::string fname = vpath.filename(); + + size_t entry_size = fuse_add_direntry(req, buf + bytes_filled, + size - bytes_filled, + fname.c_str(), &st, pos + 1); + + if(entry_size > size - bytes_filled) + break; + + bytes_filled += entry_size; + pos += 1; + } + } + + open_dir->pos(pos); // update internal position if needed + + fuse_reply_buf(req, buf, bytes_filled); + free(buf); +} + +static void +releasedir_handler(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) { + auto* ud = udata(req); + DEBUG_INFO(ud, "releasedir handler"); + if(CTX->interception_enabled() && CTX->file_map()->exist(fi->fh)) { + int ret = gkfs::syscall::gkfs_close(fd); // Close GekkoFS internal FD + + fuse_reply_err(req, ret); + return; + } + fuse_reply_err(req, 0); +} + +/// releases file descriptor, not connected to lookup_count +static void +release_handler(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) { + auto* ud = udata(req); + DEBUG_INFO(ud, "release handler"); + auto* inode = get_inode(ino); + if(!inode) { + fuse_reply_err(req, ENOENT); + return; + } + int lc = gkfs::syscall::gkfs_close(fi->fh); + if(lc < 0) { + fuse_reply_err(req, 1); + return; + } + fuse_reply_err(req, 0); +} + +/// decrement lookup count +static void +forget_handler(fuse_req_t req, fuse_ino_t ino, uint64_t nlookup) { + auto* ud = udata(req); + DEBUG_INFO(ud, "forget handler"); + + auto it = ino_map.find(ino); + if(it == ino_map.end()) { + fuse_reply_none(req); + return; + } + + Inode& inode = it->second; + if(inode.lookup_count > nlookup) + inode.lookup_count -= nlookup; + else + inode.lookup_count = 0; + + if(inode.lookup_count == 0) { // && inode.open_count == 0 + path_map.erase(inode.path); + ino_map.erase(it); + DEBUG_INFO(ud, "reached lookup_count 0"); + } + + fuse_reply_none(req); + // fuse_reply_err(req, 0); +} + +static void +flush_handler(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) { + auto* ud = udata(req); + DEBUG_INFO(ud, "flush handler"); + auto* inode = get_inode(ino); + if(!inode) { + fuse_reply_err(req, ENOENT); + return; + } + int lc = gkfs::syscall::gkfs_fsync(fi->fh); + if(lc < 0) { + fuse_reply_err(req, 1); + return; + } + fuse_reply_err(req, 0); +} + +static void +fsync_handler(fuse_req_t req, fuse_ino_t ino, int datasync, + struct fuse_file_info* fi) { + // on datasync > 0, metadata should not be flushed + flush_handler(req, ino, fi); +} + +static void +access_handler(fuse_req_t req, fuse_ino_t ino, int mask) { + auto* ud = udata(req); + DEBUG_INFO(ud, "access handler"); + if(ud->access && !ud->fifo) { + auto* inode = get_inode(ino); + if(!inode) { + fuse_reply_err(req, ENOENT); + return; + } + int lc = gkfs::syscall::gkfs_access(inode->path, mask, true); + if(lc < 0) { + fuse_reply_err(req, 1); + return; + } + fuse_reply_err(req, 0); + } else { + // deactivates following access requests and it is always treated as + // success + fuse_reply_err(req, ENOSYS); + } +} + +static void +mkdir_handler(fuse_req_t req, fuse_ino_t parent, const char* name, + mode_t mode) { + auto* ud = udata(req); + auto* parent_inode = get_inode(parent); + if(!parent_inode) { + fuse_reply_err(req, ENOENT); + return; + } + std::string path = get_path(parent_inode, name); + DEBUG_INFO(ud, "mkdir parent %s name %s", parent_inode->path.c_str(), name); + int rc = gkfs::syscall::gkfs_create(path, mode | S_IFDIR); + if(rc == -1) { + fuse_reply_err(req, 1); + return; + } + struct stat st; + int sc = gkfs::syscall::gkfs_stat(path, &st); + if(sc == -1) { + fuse_reply_err(req, 1); + return; + } + fuse_ino_t ino = alloc_inode(path); + DEBUG_INFO(ud, "create new inode ino %i", ino); + ino_map[ino].st = st; + fuse_entry_param e = {}; + e.ino = ino; + e.attr = st; + e.attr_timeout = ud->timeout; + e.entry_timeout = ud->timeout; + fuse_reply_entry(req, &e); +} + +static void +rmdir_handler(fuse_req_t req, fuse_ino_t parent, const char* name) { + auto* ud = udata(req); + auto* parent_inode = get_inode(parent); + if(!parent_inode) { + fuse_reply_err(req, ENOENT); + return; + } + std::string path = get_path(parent_inode, name); + DEBUG_INFO(ud, "rmdir %s", path.c_str()); + int rc = gkfs::syscall::gkfs_rmdir(path); + if(rc == -1) { + fuse_reply_err(req, errno); + return; + } + auto it_src = path_map.find(path); + if(it_src != path_map.end()) { + path_map.erase(it_src); + ino_map.erase(it_src->second); + } + fuse_reply_err(req, 0); +} + +static void +readlink_handler(fuse_req_t req, fuse_ino_t ino) { +#ifdef HAS_SYMLINKS + auto* inode = get_inode(ino); + if(!inode) { + fuse_reply_err(req, ENOENT); + return; + } + char link[PATH_MAX]; + int rc = gkfs::syscall::gkfs_readlink(inode->path, link, PATH_MAX); + if(rc == -1) { + fuse_reply_err(req, 1); + return; + } + link[rc] = '\0'; + fuse_reply_readlink(req, link); +#else + fuse_reply_err(req, ENOTSUP); +#endif +} + +static void +symlink_handler(fuse_req_t req, const char* linkname, fuse_ino_t parent, + const char* name) { +#ifdef HAS_SYMLINKS + auto* ud = udata(req); + DEBUG_INFO(ud, "symlink handler linkname %s name %s", linkname, name); + auto* parent_inode = get_inode(parent); + if(!parent_inode) { + DEBUG_INFO(ud, "symlink parent inode ino %i", parent); + fuse_reply_err(req, ENOENT); + return; + } + + std::string path = get_path(parent_inode, name); + std::string target = get_path(parent_inode, linkname); + + if(target.rfind(ud->mountpoint, 0) == 0) { + // starts with mount path + target = get_path(parent_inode, + target.substr(strlen(ud->mountpoint)).c_str()); + } + + DEBUG_INFO(ud, "mk symlink path %s target %s", path.c_str()); + int rc = gkfs::syscall::gkfs_mk_symlink(path, target); + if(rc < 0) { + fuse_reply_err(req, 1); + return; + } + + // Stat the new symlink so we can reply with entry info + struct stat st; + if(gkfs::syscall::gkfs_stat(path, &st) < 0) { + DEBUG_INFO(ud, "stat failed %i", errno); + fuse_reply_err(req, errno); + return; + } + DEBUG_INFO(ud, "stat mode %i, iflink %i", st.st_mode, S_IFLNK); + // TODO this meta is not saved and therefore on restart gone + // this shows the link on ls -l + st.st_mode = S_IFLNK | 0777; // mark as symlink + full perms + fuse_entry_param e = {}; + e.ino = alloc_inode(path); + e.attr = st; + st.st_size = strlen(target.c_str()); + e.attr_timeout = ud->timeout; + e.entry_timeout = ud->timeout; + fuse_reply_entry(req, &e); +#else + fuse_reply_err(req, ENOTSUP); +#endif +} + +static void +rename_handler(fuse_req_t req, fuse_ino_t old_parent, const char* old_name, + fuse_ino_t new_parent, const char* new_name, + unsigned int flags) { + +#ifdef HAS_RENAME + auto* old_parent_inode = get_inode(old_parent); + if(!old_parent_inode) { + fuse_reply_err(req, ENOENT); + return; + } + auto* new_parent_inode = get_inode(new_parent); + if(!new_parent_inode) { + fuse_reply_err(req, ENOENT); + return; + } + std::string old_path = get_path(old_parent_inode, old_name); + std::string new_path = get_path(new_parent_inode, new_name); + + struct stat st_src{}; + if(gkfs::syscall::gkfs_stat(old_path, &st_src) < 0) { + fuse_reply_err(req, errno == 0 ? ENOENT : errno); + return; + } + + struct stat st_dst{}; + const bool dst_exists = (gkfs::syscall::gkfs_stat(new_path, &st_dst) == 0); + + if((flags & RENAME_NOREPLACE) && dst_exists) { + fuse_reply_err(req, EEXIST); + return; + } + + if(flags & RENAME_EXCHANGE) { + fuse_reply_err(req, EOPNOTSUPP); + return; + } + + int rc = gkfs::syscall::gkfs_rename(old_path, new_path); + if(rc < 0) { + fuse_reply_err(req, 1); + return; + } + + fuse_ino_t src_ino = 0; + auto it_src = path_map.find(old_path); + if(it_src != path_map.end()) { + src_ino = it_src->second; + path_map.erase(it_src); + path_map[new_path] = src_ino; + ino_map[src_ino].path = new_path; + } else { + src_ino = alloc_inode(new_path); + path_map[new_path] = src_ino; + ino_map[src_ino].path = new_path; + ino_map[src_ino].st = st_src; + } + + // If destination existed and was overwritten, detach its mapping + if(dst_exists) { + auto it_dst = path_map.find(new_path); + if(it_dst != path_map.end()) { + fuse_ino_t dst_ino = it_dst->second; + path_map.erase(it_dst); + + // Mark the old dst inode as disconnected (no pathname). + // Keep it alive until lookup_count==0 and open_count==0, then free + // in forget(). + auto& dst_rec = ino_map[dst_ino]; + dst_rec.path.clear(); + } + } + + // Refresh src inode attributes under its new name + struct stat st_new{}; + if(gkfs::syscall::gkfs_stat(new_path, &st_new) == 0) { + ino_map[src_ino].st = st_new; + } + fuse_reply_err(req, 0); +#else + fuse_reply_err(req, ENOTSUP); +#endif +} + +void +mknod_handler(fuse_req_t req, fuse_ino_t parent, const char* name, mode_t mode, + dev_t rdev) { + auto* ud = udata(req); + if(!ud->fifo) { + fuse_reply_err(req, ENOTSUP); + return; + } + auto* parent_inode = get_inode(parent); + if(!parent_inode) { + fuse_reply_err(req, ENOENT); + return; + } + + mkdir(fifo_path.c_str(), 0700); + std::string path = fifo_path + "/" + name; + + if(!S_ISFIFO(mode)) { + fuse_reply_err(req, EINVAL); + return; + } + + if(mkfifo(path.c_str(), mode) == -1) { + fuse_reply_err(req, errno); + return; + } + + struct stat st{}; + if(stat(path.c_str(), &st) == -1) { + fuse_reply_err(req, errno); + return; + } + + // save synthetic entry + std::string vpath = get_path(parent_inode, name); // path in mount namespace + local_fifos[vpath] = st; + + fuse_entry_param e{}; + e.ino = st.st_ino; + e.attr = st; + e.attr_timeout = ud->timeout; + e.entry_timeout = ud->timeout; + + fuse_reply_entry(req, &e); +} + +static void +init_gekkofs() { + // TODO how to handle mount point + int res = gkfs_init(); + if(res != 0) { + printf("FUSE client failed to connect to gkfs daemon. Exit."); + exit(1); + } + + auto fl = gkfs::syscall::gkfs_get_file_list("/"); + + for(std::string s : fl) { + std::cout << s << std::endl; + } + + std::string root_path = "/"; + struct stat st; + int rc = gkfs::syscall::gkfs_stat(root_path, &st); + if(rc < 0) { + fuse_log(FUSE_LOG_ERR, "failed to open root\n"); + exit(1); + } + ino_map[FUSE_ROOT_ID] = {root_path, {}, 1}; + ino_map[FUSE_ROOT_ID].st = st; + path_map[root_path] = FUSE_ROOT_ID; + std::cout << "root node allocated" << std::endl; +} + +static void +init_ll_ops(fuse_lowlevel_ops* ops) { + // file + ops->getattr = getattr_handler; + ops->setattr = setattr_handler; + ops->open = open_handler; + ops->create = create_handler; + ops->unlink = unlink_handler; + ops->forget = forget_handler; + // ops->forget_multi + ops->readlink = readlink_handler; + ops->mknod = mknod_handler; + ops->symlink = symlink_handler; + ops->rename = rename_handler; + // ops->link + ops->flush = flush_handler; + ops->release = release_handler; + ops->fsync = fsync_handler; + // ops->write_buf + ops->lseek = lseek_handler; + + // xattr for arbitrary key value fields + // ops->setxattr + // ops->getxattr + // ops->listxattr + // ops->removexattr + + // directory + ops->lookup = lookup_handler; + ops->mkdir = mkdir_handler; + ops->rmdir = rmdir_handler; + ops->readdir = readdir_handler; + ops->opendir = opendir_handler; + ops->releasedir = releasedir_handler; + // ops->fsyncdir = nullptr; + // ops->readdirplus + + // I/O + ops->write = write_handler; + ops->read = read_handler; + + // permission + ops->access = access_handler; + + // misc + ops->init = init_handler; + ops->destroy = destroy_handler; + // ops->tmpfile // not supported in fuse < 3 + + // ops->statfs = nullptr; + // ops->flock + // ops->getlk + // ops->setlk + // ops->bmap + // ops->ioctl + // ops->poll + // ops->retrive_reply + // ops->fallocate + // ops->copy_file_range + // ops->statx // not supported in fuse < 3 +} + +void +err_cleanup1(fuse_cmdline_opts opts, fuse_args& args, struct u_data& ud) { + free(opts.mountpoint); + free(ud.mountpoint); + fuse_opt_free_args(&args); + std::cout << "# Resources released" << std::endl; +} + +void +err_cleanup2(fuse_session& se) { + fuse_session_destroy(&se); + std::cout << "# Fuse session destroyed" << std::endl; +} + +void +err_cleanup3(fuse_session& se) { + fuse_remove_signal_handlers(&se); + std::cout << "# Signal handlers removed" << std::endl; +} + +int +main(int argc, char* argv[]) { + init_ll_ops(&ll_ops); + + struct fuse_args args = FUSE_ARGS_INIT(argc, argv); + struct fuse_session* se; + struct fuse_cmdline_opts opts; + struct fuse_loop_config* config; + struct u_data ud{}; + ud.debug = 0; + ud.writeback = 0; + int ret = -1; + + /* Don't mask creation mode, kernel already did that */ + umask(0); // TODO do we need this and why? + + pthread_mutex_init(&ud.mutex, NULL); + ud.cache = CACHE_NORMAL; + + if(fuse_parse_cmdline(&args, &opts) != 0) + return 1; + if(opts.show_help) { + printf("usage: %s [options] \n\n", argv[0]); + fuse_cmdline_help(); + fuse_lowlevel_help(); + passthrough_ll_help(); + err_cleanup1(opts, args, ud); + return 0; + } else if(opts.show_version) { + printf("FUSE library version %s\n", fuse_pkgversion()); + fuse_lowlevel_version(); + ret = 0; + err_cleanup1(opts, args, ud); + return 0; + } + + if(opts.mountpoint == NULL) { + printf("usage: %s [options] \n", argv[0]); + printf(" %s --help\n", argv[0]); + ret = 1; + err_cleanup1(opts, args, ud); + return 0; + } + + if(fuse_opt_parse(&args, &ud, lo_opts, NULL) == -1) + return 1; + + ud.debug = opts.debug; + if(!ud.timeout_set) { + switch(ud.cache) { + case CACHE_NEVER: + ud.timeout = 0.0; + break; + + case CACHE_NORMAL: + ud.timeout = 1.0; + break; + + case CACHE_ALWAYS: + ud.timeout = 86400.0; + break; + } + } else if(ud.timeout < 0) { + fuse_log(FUSE_LOG_ERR, "timeout is negative (%lf)\n", ud.timeout); + exit(1); + } + + + ud.mountpoint = strdup(opts.mountpoint); + init_gekkofs(); + + + se = fuse_session_new(&args, &ll_ops, sizeof(ll_ops), &ud); + if(se == nullptr) { + err_cleanup1(opts, args, ud); + return 0; + } + + if(fuse_set_signal_handlers(se) != 0) { + err_cleanup2(*se); + err_cleanup1(opts, args, ud); + return 0; + } + + if(fuse_session_mount(se, opts.mountpoint) != 0) { + err_cleanup3(*se); + err_cleanup2(*se); + err_cleanup1(opts, args, ud); + return 0; + } + + fuse_daemonize(opts.foreground); + + /* Block until ctrl+c or fusermount -u */ + if(opts.singlethread) + ret = fuse_session_loop(se); + else { +#if FUSE_MAJOR_VERSION > 3 || \ + (FUSE_MAJOR_VERSION == 3 && FUSE_MINOR_VERSION >= 12) + config = fuse_loop_cfg_create(); + fuse_loop_cfg_set_clone_fd(config, opts.clone_fd); + fuse_loop_cfg_set_max_threads(config, opts.max_threads); + ret = fuse_session_loop_mt(se, config); + fuse_loop_cfg_destroy(config); + config = NULL; +#else + // for older fuse versions like on the ubuntu22 + ret = fuse_session_loop_mt( + se, NULL); // second argument should be checked!!! +#endif + } + + fuse_session_unmount(se); + return ret < 0 ? 1 : 0; +} diff --git a/src/client/gkfs_functions.cpp b/src/client/gkfs_functions.cpp index 95405e0cc1cd2b1134f2bc6730bf80f12355df14..b12bfe073495b37295df6d0fed6059ce4e249477 100644 --- a/src/client/gkfs_functions.cpp +++ b/src/client/gkfs_functions.cpp @@ -185,7 +185,7 @@ test_lock_file(const std::string& path) { * @param path * @param mode * @param flags - * @return 0 on success, -1 on failure + * @return fd on success, -1 on failure */ int gkfs_open(const std::string& path, mode_t mode, int flags) { @@ -809,7 +809,7 @@ gkfs_statvfs(struct statvfs* buf) { * @param fd * @param offset * @param whence - * @return 0 on success, -1 on failure + * @return position on success, -1 on failure */ off_t gkfs_lseek(unsigned int fd, off_t offset, unsigned int whence) { @@ -822,7 +822,7 @@ gkfs_lseek(unsigned int fd, off_t offset, unsigned int whence) { * @param gkfs_fd * @param offset * @param whence - * @return 0 on success, -1 on failure + * @return position on success, -1 on failure */ off_t gkfs_lseek(shared_ptr gkfs_fd, off_t offset, @@ -1451,7 +1451,7 @@ gkfs_readv(int fd, const struct iovec* iov, int iovcnt) { * wrapper function for opening directories * errno may be set * @param path - * @return 0 on success or -1 on error + * @return fd on success or -1 on error */ int gkfs_opendir(const std::string& path) { @@ -1835,6 +1835,10 @@ gkfs_mk_symlink(const std::string& path, const std::string& target_path) { errno = ENOTSUP; return -1; } + } else { + // target is not in gekkofs + errno = ENOENT; + return -1; } if(check_parent_dir(path)) { @@ -1871,7 +1875,7 @@ gkfs_mk_symlink(const std::string& path, const std::string& target_path) { * @param path * @param buf * @param bufsize - * @return 0 on success or -1 on error + * @return path size on success or -1 on error */ int gkfs_readlink(const std::string& path, char* buf, int bufsize) { diff --git a/src/client/rpc/forward_metadata.cpp b/src/client/rpc/forward_metadata.cpp index ad0882cd2b2842ccc496ba1b47ae1add0871e87d..d22842d1404a27307ae04c4c6c12f1325131f6a6 100644 --- a/src/client/rpc/forward_metadata.cpp +++ b/src/client/rpc/forward_metadata.cpp @@ -664,7 +664,7 @@ forward_get_metadentry_size(const std::string& path, const int copy) { /** * Send an RPC request to receive all entries of a directory. * @param open_dir - * @return error code + * @return error code, OpenDir */ pair> forward_get_dirents(const string& path) { @@ -780,9 +780,6 @@ forward_get_dirents(const string& path) { bool* bool_ptr = reinterpret_cast(base_ptr); char* names_ptr = reinterpret_cast(base_ptr) + (out.dirents_size() * sizeof(bool)); - // Add special files like an standard fs. - open_dir->add(".", gkfs::filemap::FileType::directory); - open_dir->add("..", gkfs::filemap::FileType::directory); for(std::size_t j = 0; j < out.dirents_size(); j++) { gkfs::filemap::FileType ftype = @@ -804,6 +801,9 @@ forward_get_dirents(const string& path) { open_dir->add(name, ftype); } } + // Add special files like an standard fs. + open_dir->add(".", gkfs::filemap::FileType::directory); + open_dir->add("..", gkfs::filemap::FileType::directory); return make_pair(err, open_dir); } diff --git a/tests/integration/CMakeLists.txt b/tests/integration/CMakeLists.txt index e7d7cccc141717f9a07031e338eeeffd4b53c81f..40204645583d82847b51f48677f274a210196a87 100644 --- a/tests/integration/CMakeLists.txt +++ b/tests/integration/CMakeLists.txt @@ -143,6 +143,15 @@ if (GKFS_RENAME_SUPPORT) ) endif () +if (GKFS_BUILD_FUSE) + gkfs_add_python_test( + NAME test_fuse_client + PYTHON_VERSION 3.6 + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/tests/integration + SOURCE fuse/ + ) +endif () + if (GKFS_INSTALL_TESTS) install(DIRECTORY harness DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/gkfs/tests/integration @@ -239,6 +248,16 @@ if (GKFS_INSTALL_TESTS) PATTERN ".pytest_cache" EXCLUDE ) endif () + + if (GKFS_BUILD_FUSE) + install(DIRECTORY fuse + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/gkfs/tests/integration + FILES_MATCHING + REGEX ".*\\.py" + PATTERN "__pycache__" EXCLUDE + PATTERN ".pytest_cache" EXCLUDE + ) + endif () endif () diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 9b3f318e6da70b0c4f9412614008d1f27b05d0fc..f0971f6905422985a144232da79e2652c22128d0 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -34,7 +34,7 @@ from pathlib import Path from harness.logger import logger, initialize_logging, finalize_logging from harness.cli import add_cli_options, set_default_log_formatter from harness.workspace import Workspace, FileCreator -from harness.gkfs import Daemon, Client, ClientLibc, Proxy, ShellClient, ShellClientLibc, FwdDaemon, FwdClient, ShellFwdClient, FwdDaemonCreator, FwdClientCreator +from harness.gkfs import Daemon, Client, ClientLibc, Proxy, ShellClient, ShellClientLibc, FwdDaemon, FwdClient, ShellFwdClient, FwdDaemonCreator, FwdClientCreator, FuseClient from harness.reporter import report_test_status, report_test_headline, report_assertion_pass def pytest_configure(config): @@ -78,7 +78,7 @@ def caplog(test_workspace, request, _caplog): def pytest_runtest_logreport(report): """ - Pytest hook called after a test phase (setup, call, teardownd) + Pytest hook called after a test phase (setup, call, teardownd) has completed. """ @@ -123,7 +123,7 @@ def gkfs_daemon(request): return request.getfixturevalue(request.param) @pytest.fixture -def gkfs_daemon_proxy(test_workspace, request): +def gkfs_daemon_proxy(test_workspace, request): interface = request.config.getoption('--interface') daemon = Daemon(interface, "rocksdb", test_workspace, True) @@ -177,7 +177,7 @@ def gkfs_shell(test_workspace): """ return ShellClient(test_workspace) - + @pytest.fixture def gkfs_shell_proxy(test_workspace): """ @@ -225,3 +225,15 @@ def gkfwd_client_factory(test_workspace): """ return FwdClientCreator(test_workspace) + +@pytest.fixture +def fuse_client(test_workspace): + """ + Sets up a gekkofs fuse client environment so that + operations (system calls, library calls, ...) can + be requested from a co-running daemon. + """ + + fuse_client = FuseClient(test_workspace) + yield fuse_client.run() + fuse_client.shutdown() diff --git a/tests/integration/conftest.template b/tests/integration/conftest.template index 9b3f318e6da70b0c4f9412614008d1f27b05d0fc..f0971f6905422985a144232da79e2652c22128d0 100644 --- a/tests/integration/conftest.template +++ b/tests/integration/conftest.template @@ -34,7 +34,7 @@ from pathlib import Path from harness.logger import logger, initialize_logging, finalize_logging from harness.cli import add_cli_options, set_default_log_formatter from harness.workspace import Workspace, FileCreator -from harness.gkfs import Daemon, Client, ClientLibc, Proxy, ShellClient, ShellClientLibc, FwdDaemon, FwdClient, ShellFwdClient, FwdDaemonCreator, FwdClientCreator +from harness.gkfs import Daemon, Client, ClientLibc, Proxy, ShellClient, ShellClientLibc, FwdDaemon, FwdClient, ShellFwdClient, FwdDaemonCreator, FwdClientCreator, FuseClient from harness.reporter import report_test_status, report_test_headline, report_assertion_pass def pytest_configure(config): @@ -78,7 +78,7 @@ def caplog(test_workspace, request, _caplog): def pytest_runtest_logreport(report): """ - Pytest hook called after a test phase (setup, call, teardownd) + Pytest hook called after a test phase (setup, call, teardownd) has completed. """ @@ -123,7 +123,7 @@ def gkfs_daemon(request): return request.getfixturevalue(request.param) @pytest.fixture -def gkfs_daemon_proxy(test_workspace, request): +def gkfs_daemon_proxy(test_workspace, request): interface = request.config.getoption('--interface') daemon = Daemon(interface, "rocksdb", test_workspace, True) @@ -177,7 +177,7 @@ def gkfs_shell(test_workspace): """ return ShellClient(test_workspace) - + @pytest.fixture def gkfs_shell_proxy(test_workspace): """ @@ -225,3 +225,15 @@ def gkfwd_client_factory(test_workspace): """ return FwdClientCreator(test_workspace) + +@pytest.fixture +def fuse_client(test_workspace): + """ + Sets up a gekkofs fuse client environment so that + operations (system calls, library calls, ...) can + be requested from a co-running daemon. + """ + + fuse_client = FuseClient(test_workspace) + yield fuse_client.run() + fuse_client.shutdown() diff --git a/tests/integration/fuse/test_basic_operations.py b/tests/integration/fuse/test_basic_operations.py new file mode 100644 index 0000000000000000000000000000000000000000..8c0142a8456f742a5348c02afd4e6cad90ba97cf --- /dev/null +++ b/tests/integration/fuse/test_basic_operations.py @@ -0,0 +1,92 @@ +################################################################################ +# Copyright 2018-2025, Barcelona Supercomputing Center (BSC), Spain # +# Copyright 2015-2025, Johannes Gutenberg Universitaet Mainz, Germany # +# # +# This software was partially supported by the # +# EC H2020 funded project NEXTGenIO (Project ID: 671951, www.nextgenio.eu). # +# # +# This software was partially supported by the # +# ADA-FS project under the SPPEXA project funded by the DFG. # +# # +# This file is part of GekkoFS. # +# # +# GekkoFS is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# GekkoFS is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with GekkoFS. If not, see . # +# # +# SPDX-License-Identifier: GPL-3.0-or-later # +################################################################################ + +import harness +from pathlib import Path +import errno +import stat +import os +import ctypes +import sh +import sys +import pytest +import time +from harness.logger import logger + +nonexisting = "nonexisting" + +# somehow are multiple test causing an error in the fuse client... +def test_read(gkfs_daemon, fuse_client): + + file = gkfs_daemon.mountdir / "file" + file2 = gkfs_daemon.mountdir / "file2" + + dir = gkfs_daemon.mountdir / "dir" + + # creation and removal + sh.bash("-c", "echo baum > " + str(file)) + assert sh.ls(fuse_client.mountdir) == "file\n" + assert sh.cat(file) == "baum\n" + sh.touch(str(file2)) + assert sh.wc("-c", str(file2)) == "0 " + str(file2) + "\n" + sh.truncate("-s", "20", str(file2)) + assert sh.wc("-c", str(file2)) == "20 " + str(file2) + "\n" + sh.mkdir(str(dir)) + assert sh.ls(fuse_client.mountdir) == "dir file file2\n" + sh.cd(str(dir)) + assert sh.pwd() == str(dir) + "\n" + sh.mkdir("-p", "foo/bar") + assert sh.ls() == "foo\n" + sh.cd("foo") + sh.rmdir("bar") + sh.cd("..") + sh.rmdir("foo") + sh.rm(str(file2)) + assert sh.ls(fuse_client.mountdir) == "dir file\n" + + # lseek test (TODO doesn't use the lseek handler) + path = gkfs_daemon.mountdir / "lseek_file" + with open(path, "wb") as f: + f.write(b"HelloWorld") # 10 bytes + fd = os.open(path, os.O_RDONLY) + pos = os.lseek(fd, 5, os.SEEK_SET) # absolute seek + assert pos == 5 + data = os.read(fd, 5) + assert data == b"World" + os.close(fd) + os.remove(path) + + # symlink + assert sh.pwd() == str(dir) + "\n" + sh.ln("-s", str(file), "link") + assert sh.ls(str(dir)) == "link\n" + assert sh.cat("link") == "baum\n" + + # renaming + sh.mv("link", "../lonk") + assert sh.ls("..") == "dir file lonk\n" diff --git a/tests/integration/harness/gkfs.py b/tests/integration/harness/gkfs.py index e9722b5edfdf385f5cda2b74d6f6bee17287f743..6b906c5870268342a037c2b7245cd963963e615a 100644 --- a/tests/integration/harness/gkfs.py +++ b/tests/integration/harness/gkfs.py @@ -74,6 +74,7 @@ gkfwd_client_log_level = 'all' gkfwd_client_log_syscall_filter = 'epoll_wait,epoll_create' gkfwd_daemon_active_log_pattern = r'Startup successful. Daemon is ready.' +gkfs_fuse_client = 'fuse_client' def get_ip_addr(iface): return netifaces.ifaddresses(iface)[netifaces.AF_INET][0]['addr'] @@ -345,7 +346,7 @@ class Daemon: logger.debug(f"daemon log file missing, checking if daemon is alive...") pid=self._proc.pid - + if not _process_exists(pid): raise RuntimeError(f"process {pid} is not running") @@ -392,7 +393,7 @@ class Proxy: self._workspace = workspace self._cmd = sh.Command(gkfs_proxy_cmd, self._workspace.bindirs) self._env = os.environ.copy() - + libdirs = ':'.join( filter(None, [os.environ.get('LD_LIBRARY_PATH', '')] + [str(p) for p in self._workspace.libdirs])) @@ -403,14 +404,14 @@ class Proxy: 'GKFS_PROXY_LOG_LEVEL': gkfs_proxy_log_level } self._env.update(self._patched_env) - + def run(self): args = ['-H', str(self.cwd / gkfs_hosts_file), '--pid-path', str(self.cwd / gkfs_proxy_pid), '-p', 'ofi+sockets'] - + logger.debug(f"spawning proxy") logger.debug(f"cmdline: {self._cmd} " + " ".join(map(str, args))) @@ -463,7 +464,7 @@ class Proxy: The maximum number of log lines to check for a match. """ - + init_time = perf_counter() @@ -479,7 +480,7 @@ class Proxy: logger.debug(f"proxy log file missing, checking if daemon is alive...") pid=self._proc.pid - + if not _process_exists(pid): raise RuntimeError(f"process {pid} is not running") @@ -514,7 +515,7 @@ class Proxy: @property def interface(self): return self._interface - + class _proxy_exec(): def __init__(self, client, name): @@ -609,7 +610,7 @@ class Client: @property def cwd(self): return self._workspace.twd - + class ClientLibc: """ A class to represent a GekkoFS client process with a patched LD_PRELOAD. @@ -901,7 +902,7 @@ class ShellClient: extra properties to it. """ - + found_cmd = shutil.which(cmd, path=':'.join(str(p) for p in self._search_paths) @@ -945,7 +946,7 @@ class ShellClient: @property def cwd(self): return self._workspace.twd - + class ShellClientLibc: """ @@ -1128,7 +1129,7 @@ class ShellClientLibc: extra properties to it. """ - + found_cmd = shutil.which(cmd, path=':'.join(str(p) for p in self._search_paths) @@ -1637,3 +1638,82 @@ class ShellFwdClient: @property def cwd(self): return self._workspace.twd + +class FuseClient: + def __init__(self, workspace): + self._workspace = workspace + #self._cmd = sh.Command("printenv", ["/usr/bin/"])#self._workspace.bindirs) + self._cmd = sh.Command(gkfs_fuse_client, self._workspace.bindirs) + self._env = os.environ.copy() + self._metadir = self.rootdir + + libdirs = ':'.join( + filter(None, [os.environ.get('LD_LIBRARY_PATH', '')] + + [str(p) for p in self._workspace.libdirs])) + + self._patched_env = { + 'LD_LIBRARY_PATH' : libdirs, + 'GKFS_HOSTS_FILE' : str(self.cwd / gkfs_hosts_file), + 'LIBGKFS_HOSTS_FILE' : str(self.cwd / gkfs_hosts_file), # TODO wtf why? see gkfs::env::HOSTS_FILE + } + self._env.update(self._patched_env) + + def run(self): + + args = [ "-f", "-s", self._workspace.mountdir, "-o", "auto_unmount" ] + + print(f"spawning fuse client") + print(f"cmdline: {self._cmd} " + " ".join(map(str, args))) + print(f"patched env:\n{pformat(self._patched_env)}") + + self._proc = self._cmd( + args, + _env=self._env, + _out='/dev/null', + _err='/dev/null', + _bg=True, + _ok_code=list(range(0, 256)) + ) + + print(f"fuse client process spawned (PID={self._proc.pid})") + time.sleep(2) # give fuse time to register mount + + return self + + def shutdown(self): + try: + self._proc.terminate() + time.sleep(1) # give fuse time to unmount + err = self._proc.wait() + except ProcessLookupError: + print("Fuse client already gone at shutdown") + pass + except sh.SignalException_SIGTERM: + pass + except Exception: + pass + + + @property + def cwd(self): + return self._workspace.twd + + @property + def rootdir(self): + return self._workspace.rootdir + + @property + def mountdir(self): + return self._workspace.mountdir + + @property + def bindirs(self): + return self._workspace.bindirs + + @property + def logdir(self): + return self._workspace.logdir + + @property + def env(self): + return self._env