commit f923f0253af63c48b2b8df7d3da6727116b20076 from: Sergey Bronnikov date: Fri Apr 30 21:20:51 2021 UTC Add configuration support Patch adds command line options and configuration file support to unreliablefs. unreliablefs can be managed in runtime using simple .INI configuration file. unreliablefs config uses configuration language which provides a structure similar to what's found in Microsoft Windows INI files or used by configparser Python module [2]. To make it possible third-party C library [1] has been imported. There is only one supported error injection - "errinj_noop" that replaces file operation with no operation. More error injections are coming. 1. https://github.com/benhoyt/inih 2. https://docs.python.org/3/library/configparser.html Closes #3 Closes #18 Closes #1 Closes #7 Closes #12 Closes #60 commit - 662240be6db343e02bfb8dba656b94a7abc721fd commit + f923f0253af63c48b2b8df7d3da6727116b20076 blob - 4e026e1b6b1eb5b9c15191dc857f1480a90ff976 blob + 8303569b2df55dc82e79c7364b66c0a9bd3bd602 --- CMakeLists.txt +++ CMakeLists.txt @@ -5,7 +5,8 @@ include(CheckFunctionExists) project (unreliablefs DESCRIPTION "A FUSE-based fault injection filesystem.") -set(UNRELIABLEFS_SRC unreliablefs.c +set(UNRELIABLEFS_SRC conf.c + unreliablefs.c unreliablefs_errinj.c unreliablefs_ops.c) @@ -56,7 +57,7 @@ add_subdirectory(tests) add_test(NAME pytest COMMAND pytest -c ${PROJECT_SOURCE_DIR}/tests/pytest.ini ${PROJECT_SOURCE_DIR}/tests/ WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}) -set(MANUAL_PAGES "unreliablefs.1") +set(MANUAL_PAGES "unreliablefs.1;unreliablefs.conf.5") add_custom_target(check DEPENDS check-mandoc) add_custom_target(check-mandoc DEPENDS ${MANUAL_PAGES}) add_custom_command(TARGET check-mandoc blob - e035e63cf465225688df2394fb4293fdff55ba09 blob + 7279944faea1fd360a270e4a5296980a9b5f2e45 --- README.md +++ README.md @@ -2,8 +2,15 @@ [![Build Status](https://api.cirrus-ci.com/github/ligurio/unreliablefs.svg)](https://cirrus-ci.com/github/ligurio/unreliablefs) -is a FUSE-based fault injection filesystem. +is a FUSE-based fault injection filesystem that allows to change +fault-injections in runtime. +Supported fault injections are: + +- `errinj_noop` - replace file operation with no operation + (similar to [libeatmydata](https://github.com/stewartsmith/libeatmydata), + but applicable to any file operation). + ### Building Prerequisites: @@ -22,7 +29,14 @@ $ cmake -DCMAKE_BUILD_TYPE=Debug .. && make -j ### Using ```sh -$ ./build/unreliablefs ~/Downloads/mnt/ -omodules=subdir,subdir=/tmp -$ ls ~/Downloads/mnt/ -$ umount /tmp/unreliable +$ mkdir /tmp/fs +$ unreliablefs /tmp/fs -base_dir=/tmp -seed=1618680646 +$ cat << EOF > /tmp/fs/unreliablefs.conf +[errinj_noop] +op_regexp = .* +path_regexp = .* +probability = 30 +EOF +$ ls -la +$ umount /tmp/fs ``` blob - b758f3fa627bd4fdfc8673c01b1241a6a66afe25 blob + 449810c16ac5e06dcc96aa3e11aba1a6e4665eea --- tests/test_unreliablefs.py +++ tests/test_unreliablefs.py @@ -43,8 +43,8 @@ def setup_unreliablefs(tmpdir): mnt_dir = str(tmpdir.mkdir('mnt')) src_dir = str(tmpdir.mkdir('src')) - modules = "-omodules=subdir,subdir={}".format(src_dir) - cmdline = base_cmdline + [ pjoin(basename, 'build/unreliablefs'), mnt_dir, modules] + options = "-basedir={}".format(src_dir) + cmdline = base_cmdline + [ pjoin(basename, 'build/unreliablefs'), mnt_dir, options ] mount_process = subprocess.Popen(cmdline) wait_for_mount(mount_process, mnt_dir) blob - /dev/null blob + b3c6dbcdf02d42abf6748168bd8a8ba57122e615 (mode 644) --- /dev/null +++ conf.c @@ -0,0 +1,298 @@ +/* inih -- simple .INI file parser + +SPDX-License-Identifier: BSD-3-Clause + +Copyright (C) 2009-2020, Ben Hoyt + +inih is released under the New BSD license (see LICENSE.txt). Go to the project +home page for more info: + +https://github.com/benhoyt/inih + +*/ + +#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) +#define _CRT_SECURE_NO_WARNINGS +#endif + +#include +#include +#include + +#include "conf.h" + +#if !INI_USE_STACK +#if INI_CUSTOM_ALLOCATOR +#include +void* ini_malloc(size_t size); +void ini_free(void* ptr); +void* ini_realloc(void* ptr, size_t size); +#else +#include +#define ini_malloc malloc +#define ini_free free +#define ini_realloc realloc +#endif +#endif + +#define MAX_SECTION 50 +#define MAX_NAME 50 + +/* Used by ini_parse_string() to keep track of string parsing state. */ +typedef struct { + const char* ptr; + size_t num_left; +} ini_parse_string_ctx; + +/* Strip whitespace chars off end of given string, in place. Return s. */ +static char* rstrip(char* s) +{ + char* p = s + strlen(s); + while (p > s && isspace((unsigned char)(*--p))) + *p = '\0'; + return s; +} + +/* Return pointer to first non-whitespace char in given string. */ +static char* lskip(const char* s) +{ + while (*s && isspace((unsigned char)(*s))) + s++; + return (char*)s; +} + +/* Return pointer to first char (of chars) or inline comment in given string, + or pointer to NUL at end of string if neither found. Inline comment must + be prefixed by a whitespace character to register as a comment. */ +static char* find_chars_or_comment(const char* s, const char* chars) +{ +#if INI_ALLOW_INLINE_COMMENTS + int was_space = 0; + while (*s && (!chars || !strchr(chars, *s)) && + !(was_space && strchr(INI_INLINE_COMMENT_PREFIXES, *s))) { + was_space = isspace((unsigned char)(*s)); + s++; + } +#else + while (*s && (!chars || !strchr(chars, *s))) { + s++; + } +#endif + return (char*)s; +} + +/* Similar to strncpy, but ensures dest (size bytes) is + NUL-terminated, and doesn't pad with NULs. */ +static char* strncpy0(char* dest, const char* src, size_t size) +{ + /* Could use strncpy internally, but it causes gcc warnings (see issue #91) */ + size_t i; + for (i = 0; i < size - 1 && src[i]; i++) + dest[i] = src[i]; + dest[i] = '\0'; + return dest; +} + +/* See documentation in header file. */ +int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler, + void* user) +{ + /* Uses a fair bit of stack (use heap instead if you need to) */ +#if INI_USE_STACK + char line[INI_MAX_LINE]; + int max_line = INI_MAX_LINE; +#else + char* line; + size_t max_line = INI_INITIAL_ALLOC; +#endif +#if INI_ALLOW_REALLOC && !INI_USE_STACK + char* new_line; + size_t offset; +#endif + char section[MAX_SECTION] = ""; + char prev_name[MAX_NAME] = ""; + + char* start; + char* end; + char* name; + char* value; + int lineno = 0; + int error = 0; + +#if !INI_USE_STACK + line = (char*)ini_malloc(INI_INITIAL_ALLOC); + if (!line) { + return -2; + } +#endif + +#if INI_HANDLER_LINENO +#define HANDLER(u, s, n, v) handler(u, s, n, v, lineno) +#else +#define HANDLER(u, s, n, v) handler(u, s, n, v) +#endif + + /* Scan through stream line by line */ + while (reader(line, (int)max_line, stream) != NULL) { +#if INI_ALLOW_REALLOC && !INI_USE_STACK + offset = strlen(line); + while (offset == max_line - 1 && line[offset - 1] != '\n') { + max_line *= 2; + if (max_line > INI_MAX_LINE) + max_line = INI_MAX_LINE; + new_line = ini_realloc(line, max_line); + if (!new_line) { + ini_free(line); + return -2; + } + line = new_line; + if (reader(line + offset, (int)(max_line - offset), stream) == NULL) + break; + if (max_line >= INI_MAX_LINE) + break; + offset += strlen(line + offset); + } +#endif + + lineno++; + + start = line; +#if INI_ALLOW_BOM + if (lineno == 1 && (unsigned char)start[0] == 0xEF && + (unsigned char)start[1] == 0xBB && + (unsigned char)start[2] == 0xBF) { + start += 3; + } +#endif + start = lskip(rstrip(start)); + + if (strchr(INI_START_COMMENT_PREFIXES, *start)) { + /* Start-of-line comment */ + } +#if INI_ALLOW_MULTILINE + else if (*prev_name && *start && start > line) { + /* Non-blank line with leading whitespace, treat as continuation + of previous name's value (as per Python configparser). */ + if (!HANDLER(user, section, prev_name, start) && !error) + error = lineno; + } +#endif + else if (*start == '[') { + /* A "[section]" line */ + end = find_chars_or_comment(start + 1, "]"); + if (*end == ']') { + *end = '\0'; + strncpy0(section, start + 1, sizeof(section)); + *prev_name = '\0'; +#if INI_CALL_HANDLER_ON_NEW_SECTION + if (!HANDLER(user, section, NULL, NULL) && !error) + error = lineno; +#endif + } + else if (!error) { + /* No ']' found on section line */ + error = lineno; + } + } + else if (*start) { + /* Not a comment, must be a name[=:]value pair */ + end = find_chars_or_comment(start, "=:"); + if (*end == '=' || *end == ':') { + *end = '\0'; + name = rstrip(start); + value = end + 1; +#if INI_ALLOW_INLINE_COMMENTS + end = find_chars_or_comment(value, NULL); + if (*end) + *end = '\0'; +#endif + value = lskip(value); + rstrip(value); + + /* Valid name[=:]value pair found, call handler */ + strncpy0(prev_name, name, sizeof(prev_name)); + if (!HANDLER(user, section, name, value) && !error) + error = lineno; + } + else if (!error) { + /* No '=' or ':' found on name[=:]value line */ +#if INI_ALLOW_NO_VALUE + *end = '\0'; + name = rstrip(start); + if (!HANDLER(user, section, name, NULL) && !error) + error = lineno; +#else + error = lineno; +#endif + } + } + +#if INI_STOP_ON_FIRST_ERROR + if (error) + break; +#endif + } + +#if !INI_USE_STACK + ini_free(line); +#endif + + return error; +} + +/* See documentation in header file. */ +int ini_parse_file(FILE* file, ini_handler handler, void* user) +{ + return ini_parse_stream((ini_reader)fgets, file, handler, user); +} + +/* See documentation in header file. */ +int ini_parse(const char* filename, ini_handler handler, void* user) +{ + FILE* file; + int error; + + file = fopen(filename, "r"); + if (!file) + return -1; + error = ini_parse_file(file, handler, user); + fclose(file); + return error; +} + +/* An ini_reader function to read the next line from a string buffer. This + is the fgets() equivalent used by ini_parse_string(). */ +static char* ini_reader_string(char* str, int num, void* stream) { + ini_parse_string_ctx* ctx = (ini_parse_string_ctx*)stream; + const char* ctx_ptr = ctx->ptr; + size_t ctx_num_left = ctx->num_left; + char* strp = str; + char c; + + if (ctx_num_left == 0 || num < 2) + return NULL; + + while (num > 1 && ctx_num_left != 0) { + c = *ctx_ptr++; + ctx_num_left--; + *strp++ = c; + if (c == '\n') + break; + num--; + } + + *strp = '\0'; + ctx->ptr = ctx_ptr; + ctx->num_left = ctx_num_left; + return str; +} + +/* See documentation in header file. */ +int ini_parse_string(const char* string, ini_handler handler, void* user) { + ini_parse_string_ctx ctx; + + ctx.ptr = string; + ctx.num_left = strlen(string); + return ini_parse_stream((ini_reader)ini_reader_string, &ctx, handler, + user); +} blob - cc0826a6abbfa403b1d8518fe2c2ec157978d90c blob + 51fdc822a71ffae9f3a772beaa456a669f63adc8 --- unreliablefs.1 +++ unreliablefs.1 @@ -9,13 +9,15 @@ .Sh SYNOPSIS .Nm mountpoint -.Op Fl h +.Op Fl basedir Ar path +.Op Fl seed Ar number +.Op Fl hvdf .Sh DESCRIPTION The .Nm is a filesystem that allows to inject errors on file operations. -It works as pass-through filesystem and redirects file operations to a file -objects on real filesystem. +Without configuration it works as pass-through filesystem and redirects file +operations to a file objects on a real filesystem. .Pp .Nm uses Filesystem in Userspace (FUSE) that allows easy setup without requiring a @@ -25,6 +27,26 @@ available unchanged. To mount filesystem it is required to specify mountpoint and after mount it will contain the same file tree as a root filesystem. .Pp +The options are as follows: +.Bl -tag -width Ds +.It Fl basedir Ar path +Specify path to a directory that should be mount. +.It Fl seed Ar number +Specify a seed. +.It Fl f +Do not daemonize. +.It Fl d +Do not daemonize. +If this option is specified, +.Nm +will run in the foreground and log to +.Em stderr . +.It Fl v +Show version. +.It Fl h +Show usage. +.El +.Pp Supported file operations are: .Xr access 2 , .Xr chmod 2 , @@ -69,16 +91,24 @@ Following functions are unsupported on OpenBSD: .Sh EXAMPLES .Bd -literal -$ mkdir /tmp/unreliable -$ unreliablefs /tmp/unreliable -$ umount /tmp/unreliable +$ mkdir /tmp/fs +$ unreliablefs /tmp/fs -basedir=/tmp -seed=1618680646 +$ cat << EOF > /tmp/fs/unreliablefs.conf +[errinj_noop] +op_regexp = .* +path_regexp = .* +probability = 30 +EOF +$ ls -la +$ umount /tmp/fs .Ed .Sh SEE ALSO .Xr fusermount 1 , .Xr errno 2 , .Xr fuse 4 , -.Xr re_format 7 , +.Xr fuse 8 , +.Xr unreliablefs.conf 5 , .Xr mount.fuse 8 .Sh AUTHORS .An -nosplit blob - /dev/null blob + 78015d141e6878398e001790f28c480d66679edd (mode 644) --- /dev/null +++ conf.h @@ -0,0 +1,157 @@ +/* inih -- simple .INI file parser + +SPDX-License-Identifier: BSD-3-Clause + +Copyright (C) 2009-2020, Ben Hoyt + +inih is released under the New BSD license (see LICENSE.txt). Go to the project +home page for more info: + +https://github.com/benhoyt/inih + +*/ + +#ifndef INI_H +#define INI_H + +/* Make this header file easier to include in C++ code */ +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/* Nonzero if ini_handler callback should accept lineno parameter. */ +#ifndef INI_HANDLER_LINENO +#define INI_HANDLER_LINENO 0 +#endif + +/* Typedef for prototype of handler function. */ +#if INI_HANDLER_LINENO +typedef int (*ini_handler)(void* user, const char* section, + const char* name, const char* value, + int lineno); +#else +typedef int (*ini_handler)(void* user, const char* section, + const char* name, const char* value); +#endif + +/* Typedef for prototype of fgets-style reader function. */ +typedef char* (*ini_reader)(char* str, int num, void* stream); + +/* Parse given INI-style file. May have [section]s, name=value pairs + (whitespace stripped), and comments starting with ';' (semicolon). Section + is "" if name=value pair parsed before any section heading. name:value + pairs are also supported as a concession to Python's configparser. + + For each name=value pair parsed, call handler function with given user + pointer as well as section, name, and value (data only valid for duration + of handler call). Handler should return nonzero on success, zero on error. + + Returns 0 on success, line number of first error on parse error (doesn't + stop on first error), -1 on file open error, or -2 on memory allocation + error (only when INI_USE_STACK is zero). +*/ +int ini_parse(const char* filename, ini_handler handler, void* user); + +/* Same as ini_parse(), but takes a FILE* instead of filename. This doesn't + close the file when it's finished -- the caller must do that. */ +int ini_parse_file(FILE* file, ini_handler handler, void* user); + +/* Same as ini_parse(), but takes an ini_reader function pointer instead of + filename. Used for implementing custom or string-based I/O (see also + ini_parse_string). */ +int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler, + void* user); + +/* Same as ini_parse(), but takes a zero-terminated string with the INI data +instead of a file. Useful for parsing INI data from a network socket or +already in memory. */ +int ini_parse_string(const char* string, ini_handler handler, void* user); + +/* Nonzero to allow multi-line value parsing, in the style of Python's + configparser. If allowed, ini_parse() will call the handler with the same + name for each subsequent line parsed. */ +#ifndef INI_ALLOW_MULTILINE +#define INI_ALLOW_MULTILINE 1 +#endif + +/* Nonzero to allow a UTF-8 BOM sequence (0xEF 0xBB 0xBF) at the start of + the file. See https://github.com/benhoyt/inih/issues/21 */ +#ifndef INI_ALLOW_BOM +#define INI_ALLOW_BOM 1 +#endif + +/* Chars that begin a start-of-line comment. Per Python configparser, allow + both ; and # comments at the start of a line by default. */ +#ifndef INI_START_COMMENT_PREFIXES +#define INI_START_COMMENT_PREFIXES ";#" +#endif + +/* Nonzero to allow inline comments (with valid inline comment characters + specified by INI_INLINE_COMMENT_PREFIXES). Set to 0 to turn off and match + Python 3.2+ configparser behaviour. */ +#ifndef INI_ALLOW_INLINE_COMMENTS +#define INI_ALLOW_INLINE_COMMENTS 1 +#endif +#ifndef INI_INLINE_COMMENT_PREFIXES +#define INI_INLINE_COMMENT_PREFIXES ";" +#endif + +/* Nonzero to use stack for line buffer, zero to use heap (malloc/free). */ +#ifndef INI_USE_STACK +#define INI_USE_STACK 1 +#endif + +/* Maximum line length for any line in INI file (stack or heap). Note that + this must be 3 more than the longest line (due to '\r', '\n', and '\0'). */ +#ifndef INI_MAX_LINE +#define INI_MAX_LINE 200 +#endif + +/* Nonzero to allow heap line buffer to grow via realloc(), zero for a + fixed-size buffer of INI_MAX_LINE bytes. Only applies if INI_USE_STACK is + zero. */ +#ifndef INI_ALLOW_REALLOC +#define INI_ALLOW_REALLOC 0 +#endif + +/* Initial size in bytes for heap line buffer. Only applies if INI_USE_STACK + is zero. */ +#ifndef INI_INITIAL_ALLOC +#define INI_INITIAL_ALLOC 200 +#endif + +/* Stop parsing on first error (default is to keep parsing). */ +#ifndef INI_STOP_ON_FIRST_ERROR +#define INI_STOP_ON_FIRST_ERROR 0 +#endif + +/* Nonzero to call the handler at the start of each new section (with + name and value NULL). Default is to only call the handler on + each name=value pair. */ +#ifndef INI_CALL_HANDLER_ON_NEW_SECTION +#define INI_CALL_HANDLER_ON_NEW_SECTION 0 +#endif + +/* Nonzero to allow a name without a value (no '=' or ':' on the line) and + call the handler with value NULL in this case. Default is to treat + no-value lines as an error. */ +#ifndef INI_ALLOW_NO_VALUE +#define INI_ALLOW_NO_VALUE 0 +#endif + +/* Nonzero to use custom ini_malloc, ini_free, and ini_realloc memory + allocation functions (INI_USE_STACK must also be 0). These functions must + have the same signatures as malloc/free/realloc and behave in a similar + way. ini_realloc is only needed if INI_ALLOW_REALLOC is set. */ +#ifndef INI_CUSTOM_ALLOCATOR +#define INI_CUSTOM_ALLOCATOR 0 +#endif + + +#ifdef __cplusplus +} +#endif + +#endif /* INI_H */ blob - 8d5d93bb0ed6d1e0e4293a0a3ed38987a23ada8f blob + c64750ff018d6a77dfceaf9bf7b20d0a9f53f40b --- unreliablefs.c +++ unreliablefs.c @@ -1,12 +1,21 @@ #define FUSE_USE_VERSION 29 #include +#include #include #include #include +#include +#include +#include + #include "unreliablefs_ops.h" +#include "unreliablefs.h" +extern struct err_inj_q *config_init(const char* conf_path); +extern void config_delete(struct err_inj_q *config); + static struct fuse_operations unreliable_ops = { .getattr = unreliable_getattr, .readlink = unreliable_readlink, @@ -60,30 +69,120 @@ static struct fuse_operations unreliable_ops = { #endif /* HAVE_UTIMENSAT */ }; +enum { + KEY_HELP, + KEY_VERSION, + KEY_DEBUG, +}; + +#define UNRELIABLEFS_OPT(t, p, v) { t, offsetof(struct unreliablefs_config, p), v } +#define UNRELIABLEFS_VERSION "0.1" + +static struct fuse_opt unreliablefs_opts[] = { + UNRELIABLEFS_OPT("-seed=%u", seed, 0), + UNRELIABLEFS_OPT("-basedir=%s", basedir, 0), + + FUSE_OPT_KEY("-d", KEY_DEBUG), + FUSE_OPT_KEY("-V", KEY_VERSION), + FUSE_OPT_KEY("-v", KEY_VERSION), + FUSE_OPT_KEY("--version", KEY_VERSION), + FUSE_OPT_KEY("-h", KEY_HELP), + FUSE_OPT_KEY("--help", KEY_HELP), + FUSE_OPT_KEY("subdir", FUSE_OPT_KEY_DISCARD), + FUSE_OPT_KEY("modules=", FUSE_OPT_KEY_DISCARD), + FUSE_OPT_END +}; + +static int unreliablefs_opt_proc(void *data, const char *arg, int key, struct fuse_args *outargs) +{ + switch (key) { + case KEY_HELP: + fprintf(stderr, + "usage: unreliablefs mountpoint [options]\n\n" + "general options:\n" + " -h --help print help\n" + " -v --version print version\n" + " -d enable debug output (implies -f)\n" + " -f foreground operation\n\n" + "unreliablefs options:\n" + " -seed=NUM random seed\n" + " -basedir=STRING directory to mount\n\n"); + exit(1); + + case KEY_VERSION: + fprintf(stderr, "unreliablefs version %s\n", UNRELIABLEFS_VERSION); + fuse_opt_add_arg(outargs, "--version"); + fuse_main(outargs->argc, outargs->argv, &unreliable_ops, NULL); + exit(1); + } + return 1; +} + +int is_dir(const char *path) { + struct stat statbuf; + if (stat(path, &statbuf) != 0) { + return 0; + } + + return S_ISDIR(statbuf.st_mode); +} + int main(int argc, char *argv[]) { -#if defined(DEBUG) - int fuse_argc = 3; - char *fuse_argv[fuse_argc]; + struct fuse_args args = FUSE_ARGS_INIT(argc, argv); + memset(&conf, 0, sizeof(conf)); + conf.seed = time(0); + conf.basedir = "/"; + fuse_opt_parse(&args, &conf, unreliablefs_opts, unreliablefs_opt_proc); + srand(conf.seed); + fprintf(stdout, "random seed = %d\n", conf.seed); - if (argc != 2) { - fprintf(stderr, "Usage: %s MOUNTPOINT\n", argv[0]); - return EXIT_FAILURE; + if (is_dir(conf.basedir) == 0) { + fprintf(stderr, "basedir ('%s') is not a directory\n", conf.basedir); + fuse_opt_free_args(&args); + return EXIT_FAILURE; } + char subdir_option[PATH_MAX]; + sprintf(subdir_option, "-omodules=subdir,subdir=%s", conf.basedir); + fuse_opt_add_arg(&args, subdir_option); + /* build config_path */ + char *real_path = realpath(conf.basedir, NULL); + if (!real_path) { + perror("realpath"); + fuse_opt_free_args(&args); + return EXIT_FAILURE; + } + conf.basedir = real_path; + size_t sz = strlen(DEFAULT_CONF_NAME) + strlen(conf.basedir) + 2; + conf.config_path = malloc(sz); + if (!conf.config_path) { + perror("malloc"); + fuse_opt_free_args(&args); + return EXIT_FAILURE; + } + /* read configuration file on start */ + snprintf(conf.config_path, sz, "%s/%s", conf.basedir, DEFAULT_CONF_NAME); + conf.errors = config_init(conf.config_path); + if (!conf.errors) { + fprintf(stdout, "error injections are not configured!\n"); + } + if (pthread_mutex_init(&conf.mutex, NULL) != 0) { + fuse_opt_free_args(&args); + perror("pthread_mutex_init"); + return EXIT_FAILURE; + } - fuse_argv[0] = argv[0]; - fuse_argv[1] = argv[1]; - #if FUSE_USE_VERSION < 30 - fuse_argv[2] = "-ononempty,suid,dev,allow_other,default_permissions"; - #else - fuse_argv[2] = "-osuid,dev,allow_other,default_permissions"; - #endif - fuse_argv[3] = NULL; + fprintf(stdout, "starting FUSE filesystem unreliablefs\n"); + int ret = fuse_main(args.argc, args.argv, &unreliable_ops, NULL); - fprintf(stdout, "Starting FUSE filesystem\n"); - return fuse_main(fuse_argc, fuse_argv, &unreliable_ops, NULL); -#else - fprintf(stdout, "Starting FUSE filesystem\n"); - return fuse_main(argc, argv, &unreliable_ops, NULL); -#endif /* DEBUG */ + /* cleanup */ + fuse_opt_free_args(&args); + config_delete(conf.errors); + if (conf.config_path) + free(conf.config_path); + if (!ret) { + fprintf(stdout, "random seed = %d\n", conf.seed); + } + + return ret; } blob - 81ead53e35061f31ac181a6bc17c9241bca107f8 blob + ca7bc1fd4dd53ea18989ab28d6cbd9f2b3830f8d --- unreliablefs_errinj.c +++ unreliablefs_errinj.c @@ -1,16 +1,17 @@ #define FUSE_USE_VERSION 29 -#include +#include +#include /* basename() and dirname() */ +#include #include +#include +#include -#if !defined(__OpenBSD__) && !defined(__FreeBSD__) && !defined(__APPLE__) -#define MAX_ERR EXFULL -#else -#define MAX_ERR ELAST -#endif /* __OpenBSD__ */ -#define MIN_ERR E2BIG +#include -#define PROBABILITY 5 +#include "conf.h" +#include "unreliablefs.h" +#include "unreliablefs_errinj.h" static int rand_range(int min_n, int max_n) { @@ -19,15 +20,157 @@ static int rand_range(int min_n, int max_n) int error_inject(const char* path, char* operation) { -#if defined(DEBUG) - int err_no = 0; - int p = rand_range(0, 100); - if ((p >= 0) && (p <= PROBABILITY)) { - err_no = rand_range(MIN_ERR, MAX_ERR); + int rc = -0; + struct errinj_conf *err; + /* read configuration file on change */ + pthread_mutex_lock(&conf.mutex); + if (strcmp(path, conf.config_path) == 0) { + config_delete(conf.errors); + conf.errors = config_init(path); } + if (!conf.errors) { + goto cleanup; + } - return -err_no; -#else - return 0; -#endif /* DEBUG */ + /* apply error injections defined in configuration one by one */ + TAILQ_FOREACH(err, conf.errors, entries) { + if (is_regex_matched(err->path_regexp, path) != 0) { + fprintf(stderr, "errinj '%s' skipped: path_regexp (%s) is not matched\n", + errinj_name[err->type], err->path_regexp); + continue; + } + if (is_regex_matched(err->op_regexp, operation) != 0) { + fprintf(stderr, "errinj '%s' skipped: op_regexp (%s) is not matched\n", + errinj_name[err->type], err->op_regexp); + continue; + } + int p = rand_range(MIN_PROBABLITY, MAX_PROBABLITY); + if (!((p >= 0) && (p <= err->probability))) { + fprintf(stderr, "errinj '%s' skipped: probability (%d) is not matched\n", + errinj_name[err->type], err->probability); + continue; + } + switch (err->type) { + case ERRINJ_NOOP: + fprintf(stderr, "%s triggered on operation '%s', %s\n", + errinj_name[err->type], operation, path); + rc = 0; + break; + } + } + +cleanup: + pthread_mutex_unlock(&conf.mutex); + return rc; } + +static errinj_type errinj_type_by_name(const char *name) +{ + int idx = 0; + int n_elem = sizeof(errinj_name)/sizeof(errinj_name[0]); + for (int i = 0; i < n_elem; i++) { + if (strcmp(errinj_name[i], name) == 0) + idx = i; + } + + return (errinj_type)idx; +} + +struct err_inj_q *config_init(const char* conf_path) { + fprintf(stdout, "read configuration %s\n", conf_path); + struct err_inj_q *errors = calloc(1, sizeof(struct err_inj_q)); + if (!errors) { + perror("calloc"); + return NULL; + } + TAILQ_INIT(errors); /* initialize queue */ + if (access(conf_path, F_OK ) == 0) { + if (ini_parse(conf_path, conf_option_handler, errors) < 0) { + fprintf(stderr, "can't load '%s'\n", conf_path); + return NULL; + } + } + + return errors; +} + +void config_delete(struct err_inj_q *errors) { + if (!errors) { + return; + } + errinj_conf *err; + /* delete configuration of error injections */ + while ((err= TAILQ_FIRST(errors))) { + if (err->path_regexp) + free((char*)err->path_regexp); + if (err->op_regexp) + free((char*)err->op_regexp); + TAILQ_REMOVE(errors, err, entries); + free(err); + } + free(errors); +} + +int conf_option_handler(void* cfg, const char* section, + const char* key, const char* value) +{ + errinj_conf *err = NULL; + errinj_type cur_type = errinj_type_by_name(section); + errinj_conf *np; + int is_errinj_found = 0; + TAILQ_FOREACH(np, (struct err_inj_q *)cfg, entries) { + if (np->type == cur_type) { + err = np; + is_errinj_found = 1; + break; + } + } + if (!err) { + if ((err = calloc(1, sizeof(struct errinj_conf))) == NULL) { + perror("calloc"); + return -1; + } + err->type = cur_type; + } + + if (is_errinj_found != 1) { + TAILQ_INSERT_TAIL((struct err_inj_q *)cfg, err, entries); + fprintf(stdout, "enabled error injection %s\n", section); + } + fprintf(stdout, "[%s] %s = %s\n", section, key, value); + if (strcmp(key, "path_regexp") == 0) { + if (err->path_regexp) + free(err->path_regexp); + err->path_regexp = strdup(value); + } else if (strcmp(key, "op_regexp") == 0) { + if (err->op_regexp) + free(err->op_regexp); + err->op_regexp = strdup(value); + } else if (strcmp(key, "probability") == 0) { + err->probability = atoi(value); + } else { + fprintf(stderr, "unknown option '%s' in configuration file\n", key); + return 0; + } + + return 1; +} + +int is_regex_matched(const char *regex, const char *string) { + if (!regex || !string) + return 0; + regex_t reg; + regmatch_t match[1]; + int rc = regcomp(®, regex, REG_ICASE | REG_EXTENDED); + if (rc != 0) { + perror("regcomp"); + regfree(®); + return rc; + } + rc = regexec(®, string, 1, match, 0); + if (rc != 0) { + perror("regexec"); + } + regfree(®); + return rc; +} blob - 0fcd5ef17344fb7ceb8744475e73e40ea1b54457 blob + 8f3a081777a733e4d6dd713f3d88eb1e31f5cc02 --- unreliablefs_errinj.h +++ unreliablefs_errinj.h @@ -1,6 +1,48 @@ #ifndef ERRINJ_HH #define ERRINJ_HH +#include + +#define MAX_ERRINJ_NAME_LENGTH 20 + +#if !defined(__OpenBSD__) && !defined(__FreeBSD__) && !defined(__APPLE__) +#define MAX_ERRNO EXFULL +#else +#define MAX_ERRNO ELAST +#endif /* __OpenBSD__ */ +#define MIN_ERRNO E2BIG + +#define MIN_PROBABLITY 0 +#define MAX_PROBABLITY 100 + int error_inject(const char* path, char* operation); +struct err_inj_q *config_init(const char* conf_path); +void config_delete(struct err_inj_q *config); +int conf_option_handler(void* cfg, const char* section, + const char* name, const char* value); +int is_regex_matched(const char *regex, const char *string); +const char *errinj_name[] = +{ + "errinj_noop", +}; + +typedef enum { + ERRINJ_NOOP, +} errinj_type; + +typedef struct errinj_conf errinj_conf; + +struct errinj_conf { + char *err_injection_name; + char *op_regexp; + char *path_regexp; + int probability; + errinj_type type; + + TAILQ_ENTRY(errinj_conf) entries; +}; + +TAILQ_HEAD(err_inj_q, errinj_conf); + #endif /* ERRINJ_HH */ blob - d69f69870fab9e88a902ea7c428d8ebf01f3543e blob + d9e2c0ff05d0733be8dd43c85f57154ef9ae5f96 --- unreliablefs_ops.c +++ unreliablefs_ops.c @@ -16,9 +16,10 @@ #define _XOPEN_SOURCE 700 #endif -#include "unreliablefs_errinj.h" #include "unreliablefs_ops.h" +extern int error_inject(const char* path, char* operation); + int unreliable_lstat(const char *path, struct stat *buf) { int ret = error_inject(path, "lstat"); blob - /dev/null blob + a27f32bbc1825578e040a54c9326b06fea056b87 (mode 644) --- /dev/null +++ unreliablefs.conf.5 @@ -0,0 +1,73 @@ +.\" Copyright (c) 2021 Sergey Bronnikov +.\" +.Dd $Mdocdate: April 15 2021 $ +.Dt UNRELIABLEFS.CONF 5 +.Os +.Sh NAME +.Nm unreliablefs.conf +.Nd format of the configuration file used by +.Xr unreliablefs 1 +.Sh DESCRIPTION +The configuration file format is quite simple. +Sections are delimited by square brackets: +.Pp +.Rs +[Section] +.Re +.Pp +And options within brackets sections are simple key value pairs: +.Pp +.Rs +Option = Value +.Re +.Sh OPTIONS +Per-fault-injection customizable variables are specified within sections +with section names matching the fault-injection name. +.Pp +Supported fault injections are: +.Bl -tag -width Ds +.It Cm errinj_noop +File operation replaced with no-op. +.El +.Pp +The options are: +.Bl -tag -width Ds +.It Cm op_regexp +Sets the regular expression that matches file operation for what fault injection is applicable. +Option uses format of regular expressions described in +.Xr re_format 7 . +POSIX Extended Regular Expression syntax is supported and regular expressions do not differentiate case. +.It Cm path_regexp +Sets the regular expression that matches paths where fault injection is applicable. +Option uses format of regular expressions described in +.Xr re_format 7 . +POSIX Extended Regular Expression syntax is supported and regular expressions do not differentiate case. +.It Cm probability +Sets the probability in percents. +Probability equal to 0 means that error injection will never happen. +Probability equal to 100 means that error injection will happen on each file operation. +.El +.Sh EXAMPLES +.Bd -literal + +[errinj_noop] +path_regexp = .* +op_regexp = .* +probability = 70 + +.Ed +.Sh SEE ALSO +.Xr unreliablefs 1 , +.Xr errno 2 , +.Xr syslog 3 , +.Xr re_format 7 +.Sh AUTHORS +.An -nosplit +The +.Xr unreliablefs 1 +utility was written by +.An Sergey +.An Bronnikov . +.\" .Sh HISTORY +.\" .Sh BUGS +.\" .Sh CAVEATS blob - /dev/null blob + 90b3eb1b21a0708a0631e25873275fe3ecdd591f (mode 644) --- /dev/null +++ unreliablefs.h @@ -0,0 +1,20 @@ +#ifndef UNRELIABLEFS_HH +#define UNRELIABLEFS_HH + +#include /* PATH_MAX */ +#include + +#define DEFAULT_CONF_NAME "unreliablefs.conf" + +typedef struct unreliablefs_config { + struct err_inj_q *errors; + char *basedir; + char *config_path; + unsigned int seed; + unsigned int debug; + pthread_mutex_t mutex; +} unreliablefs_config; + +struct unreliablefs_config conf; + +#endif /* UNRELIABLEFS_HH */