From b4bc1c68f70680f75985be40b8e3b0aa4d919ed1 Mon Sep 17 00:00:00 2001 From: Colin Patrick McCabe Date: Tue, 31 May 2011 15:47:13 -0700 Subject: [PATCH] rados export: better name mangling rules, fix test Introduce a versioning scheme for name mangling, so that we can change it in the future if we want to. For names that require no mangling, don't do anything (don't add any hash at the end, etc.) Make the hash 16 bytes long rather than 8 bytes. Fix the unit test to take into account the new rules. Signed-off-by: Colin McCabe --- src/rados_sync.cc | 362 ++++++++++++++++++++---------------- src/test/test_rados_tool.sh | 39 ++-- 2 files changed, 226 insertions(+), 175 deletions(-) diff --git a/src/rados_sync.cc b/src/rados_sync.cc index 1551240deb166..8e2b06f948a45 100644 --- a/src/rados_sync.cc +++ b/src/rados_sync.cc @@ -12,15 +12,18 @@ * */ -#include +#define __STDC_FORMAT_MACROS + #include #include #include +#include #include #include #include #include #include +#include #include #include @@ -28,11 +31,13 @@ #include "common/common_init.h" #include "common/config.h" #include "common/errno.h" +#include "common/strtol.h" #include "include/rados/librados.hpp" using std::auto_ptr; using namespace librados; +static const char * const XATTR_RADOS_SYNC_VER = "user.rados_sync_ver"; static const char * const XATTR_FULLNAME = "user.rados_full_name"; static const char USER_XATTR_PREFIX[] = "user.rados."; static const size_t USER_XATTR_PREFIX_LEN = @@ -46,70 +51,6 @@ static const char ERR_PREFIX[] = "[ERROR] "; #define ENOATTR ENODATA #endif -static int xattr_test(const char *dir_name) -{ - int ret; - int fd = -1; - const char file_name[] = "/xattr_test_file"; - char path[strlen(dir_name) + strlen(file_name) + 2]; - snprintf(path, sizeof(path), "%s/%s", dir_name, file_name); - - char buf[] = "12345"; - char buf2[sizeof(buf)]; - memset(buf2, 0, sizeof(buf2)); - - fd = open(path, O_CREAT | O_TRUNC | O_WRONLY, 0700); - if (fd < 0) { - ret = errno; - cerr << ERR_PREFIX << "xattr_test: unable to open '" << path << "': " - << cpp_strerror(ret) << std::endl; - goto done; - } - ret = fsetxattr(fd, XATTR_FULLNAME, buf, sizeof(buf), 0); - if (ret) { - ret = errno; - cerr << ERR_PREFIX << "xattr_test: fsetxattr failed with error " - << cpp_strerror(ret) << std::endl; - goto done; - } - if (close(fd) < 0) { - ret = errno; - fd = -1; - cerr << ERR_PREFIX << "xattr_test: close failed with error " - << cpp_strerror(ret) << std::endl; - goto done; - } - fd = -1; - ret = getxattr(path, XATTR_FULLNAME, buf2, sizeof(buf2)); - if (ret < 0) { - ret = errno; - cerr << ERR_PREFIX << "xattr_test: fgetxattr failed with error " - << cpp_strerror(ret) << std::endl; - goto done; - } - if (memcmp(buf, buf2, sizeof(buf))) { - ret = ENOTSUP; - cerr << ERR_PREFIX << "xattr_test: failed to read back the same xattr " - << "value that we set." << std::endl; - goto done; - } - ret = 0; - -done: - if (fd >= 0) { - close(fd); - fd = -1; - } - unlink(path); - if (ret) { - cerr << ERR_PREFIX << "xattr_test: the filesystem at " << dir_name << " does " - << "not appear to support extended attributes. Please remount your " - << "filesystem with extended attributes enabled, or pick a different " - << "directory." << std::endl; - } - return ret; -} - /* Given the name of an extended attribute from a file in the filesystem, * returns an empty string if the extended attribute does not represent a rados * user extended attribute. Otherwise, returns the name of the rados extended @@ -124,6 +65,182 @@ static std::string get_user_xattr_name(const char *fs_xattr_name) return fs_xattr_name + USER_XATTR_PREFIX_LEN; } +/* Represents a directory in the filesystem that we export rados objects to (or + * import them from.) + */ +class ExportDir +{ +public: + static ExportDir* create_for_writing(const std::string path, int version, + bool create) + { + if (access(path.c_str(), R_OK | W_OK) == 0) { + return ExportDir::from_file_system(path); + } + if (!create) { + cerr << ERR_PREFIX << "ExportDir: directory '" + << path << "' does not exist. Use --create to create it." + << std::endl; + return NULL; + } + int ret = mkdir(path.c_str(), 0700); + if (ret < 0) { + int err = errno; + if (err != EEXIST) { + cerr << ERR_PREFIX << "ExportDir: mkdir error: " + << cpp_strerror(err) << std::endl; + return NULL; + } + } + char buf[32]; + snprintf(buf, sizeof(buf), "%d", version); + ret = setxattr(path.c_str(), XATTR_RADOS_SYNC_VER, buf, strlen(buf) + 1, 0); + if (ret < 0) { + int err = errno; + cerr << ERR_PREFIX << "ExportDir: setxattr error :" + << cpp_strerror(err) << std::endl; + return NULL; + } + return new ExportDir(version, path); + } + + static ExportDir* from_file_system(const std::string path) + { + if (access(path.c_str(), R_OK)) { + cerr << "ExportDir: source directory '" << path + << "' appears to be inaccessible." << std::endl; + return NULL; + } + int ret; + char buf[32]; + memset(buf, 0, sizeof(buf)); + ret = getxattr(path.c_str(), XATTR_RADOS_SYNC_VER, buf, sizeof(buf) - 1); + if (ret < 0) { + ret = errno; + if (ret == ENODATA) { + cerr << ERR_PREFIX << "ExportDir: directory '" << path + << "' does not appear to have been created by a rados " + << "export operation." << std::endl; + return NULL; + } + cerr << ERR_PREFIX << "ExportDir: getxattr error :" + << cpp_strerror(ret) << std::endl; + return NULL; + } + std::string err; + ret = strict_strtol(buf, 10, &err); + if (!err.empty()) { + cerr << ERR_PREFIX << "ExportDir: invalid value for " + << XATTR_RADOS_SYNC_VER << ": " << buf << ". parse error: " + << err << std::endl; + return NULL; + } + if (ret != 1) { + cerr << ERR_PREFIX << "ExportDir: can't handle any naming " + << "convention besides version 1. You must upgrade this program to " + << "handle the data in the new format." << std::endl; + return NULL; + } + return new ExportDir(ret, path); + } + + /* Given a rados object name, return something which looks kind of like the + * first part of the name. + * + * The actual file name that the backed-up object is stored in is irrelevant + * to rados_sync. The only reason to make it human-readable at all is to make + * things easier on sysadmins. The XATTR_FULLNAME extended attribute has the + * real, full object name. + * + * This function turns unicode into a bunch of 'at' signs. This could be + * fixed. If you try, be sure to handle all the multibyte characters + * correctly. + * I guess a better hash would be nice too. + */ + std::string get_fs_path(const std::string rados_name) const + { + static int HASH_LENGTH = 17; + size_t i; + size_t strlen_rados_name = strlen(rados_name.c_str()); + size_t sz; + bool need_hash = false; + if (strlen_rados_name > 200) { + sz = 200; + need_hash = true; + } + else { + sz = strlen_rados_name; + } + char fs_path[sz + HASH_LENGTH + 1]; + for (i = 0; i < sz; ++i) { + // Just replace anything that looks funny with an 'at' sign. + // Unicode also gets turned into 'at' signs. + signed char c = rados_name[i]; + if (c < 0x20) { + // Since c is signed, this also eliminates bytes with the high bit set + c = '@'; + need_hash = true; + } + else if (c == 0x7f) { + c = '@'; + need_hash = true; + } + else if (c == '/') { + c = '@'; + need_hash = true; + } + else if (c == '\\') { + c = '@'; + need_hash = true; + } + else if (c == ' ') { + c = '_'; + need_hash = true; + } + else if (c == '.') { + c = '@'; + need_hash = true; + } + else if (c == '\n') { + c = '@'; + need_hash = true; + } + else if (c == '\r') { + c = '@'; + need_hash = true; + } + fs_path[i] = c; + } + + if (need_hash) { + uint64_t hash = 17; + for (i = 0; i < strlen_rados_name; ++i) { + hash += (rados_name[i] * 33); + } + // The extra byte of length is because snprintf always NULL-terminates. + snprintf(fs_path + i, HASH_LENGTH + 1, "_%016" PRIx64, hash); + } + else { + // NULL-terminate. + fs_path[i] = '\0'; + } + + ostringstream oss; + oss << path << "/" << fs_path; + return oss.str(); + } + +private: + ExportDir(int version_, const std::string path_) + : version(version_), + path(path_) + { + } + + int version; + std::string path; +}; + class DirHolder { public: DirHolder() @@ -217,8 +334,8 @@ public: cerr << ERR_PREFIX << "BackedUpObject::from_path: found empty " << XATTR_FULLNAME << " attribute on '" << path << "'" << std::endl; - ret = ENOATTR; - } else if (ret == ENOATTR) { + ret = ENODATA; + } else if (ret == ENODATA) { cerr << ERR_PREFIX << "BackedUpObject::from_path: there was no " << XATTR_FULLNAME << " attribute found on '" << path << "'" << std::endl; @@ -296,57 +413,10 @@ public: free(rados_name); } - /* Given a rados object name, return something which looks kind of like the - * first part of the name. - * - * The actual file name that the backed-up object is stored in is irrelevant - * to rados_sync. The only reason to make it human-readable at all is to make - * things easier on sysadmins. The XATTR_FULLNAME extended attribute has the - * real, full object name. - * - * This function turns unicode into a bunch of 'at' signs. This could be - * fixed. If you try, be sure to handle all the multibyte characters - * correctly. - * I guess a better hash would be nice too. - */ - std::string get_fs_path(const char *dir_name) const + /* Get the mangled name for this rados object. */ + std::string get_fs_path(const ExportDir *export_dir) const { - size_t i; - uint32_t hash = 0; - size_t sz = strlen(rados_name); - for (i = 0; i < sz; ++i) { - hash += (rados_name[i] * 33); - } - if (sz > 200) - sz = 200; - char fs_path[9 + sz + 1]; - sprintf(fs_path, "%08x_", hash); - for (i = 0; i < sz; ++i) { - // Just replace anything that looks funny with an 'at' sign. - // Unicode also gets turned into 'at' signs. - char c = rados_name[i]; - if (c < 0x20) // also eliminate bytes with the high bit set - c = '@'; - else if (c == 0x7f) - c = '@'; - else if (c == '/') - c = '@'; - else if (c == '\\') - c = '@'; - else if (c == ' ') - c = '_'; - else if (c == '.') - c = '@'; - else if (c == '\n') - c = '@'; - else if (c == '\r') - c = '@'; - fs_path[9 + i] = c; - } - fs_path[9 + i] = '\0'; - ostringstream oss; - oss << dir_name << "/" << fs_path; - return oss.str(); + return export_dir->get_fs_path(rados_name); } /* Convert the xattrs on this BackedUpObject to a kind of JSON-like string. @@ -642,7 +712,7 @@ private: free(data); return ENOBUFS; } - xattrs[USER_XATTR_PREFIX + i->first] = xattr; + xattrs[i->first] = xattr; attrset.erase(i++); } return 0; @@ -659,11 +729,15 @@ private: }; static int do_export(IoCtx& io_ctx, const char *dir_name, - bool force, bool delete_after) + bool create, bool force, bool delete_after) { int ret; librados::ObjectIterator oi = io_ctx.objects_begin(); librados::ObjectIterator oi_end = io_ctx.objects_end(); + auto_ptr export_dir; + export_dir.reset(ExportDir::create_for_writing(dir_name, 1, create)); + if (!export_dir.get()) + return -EIO; for (; oi != oi_end; ++oi) { enum { CHANGED_XATTRS = 0x1, @@ -682,7 +756,7 @@ static int do_export(IoCtx& io_ctx, const char *dir_name, << ret << std::endl; return ret; } - std::string obj_path(sobj->get_fs_path(dir_name)); + std::string obj_path(sobj->get_fs_path(export_dir.get())); if (force) { flags |= (CHANGED_CONTENTS | CHANGED_XATTRS); } @@ -721,7 +795,10 @@ static int do_export(IoCtx& io_ctx, const char *dir_name, cerr << ERR_PREFIX << "internal error on line: " << __LINE__ << std::endl; return -ENOSYS; } - ret = setxattr(obj_path.c_str(), x->c_str(), xattr->data, xattr->len, 0); + std::string xattr_fs_name(USER_XATTR_PREFIX); + xattr_fs_name += x->c_str(); + ret = setxattr(obj_path.c_str(), xattr_fs_name.c_str(), + xattr->data, xattr->len, 0); if (ret) { ret = errno; cerr << ERR_PREFIX << "setxattr error: " << cpp_strerror(ret) << std::endl; @@ -797,14 +874,12 @@ static int do_export(IoCtx& io_ctx, const char *dir_name, static int do_import(IoCtx& io_ctx, const char *dir_name, bool force, bool delete_after) { - int ret = mkdir(dir_name, 0700); - if (ret < 0) { - int err = errno; - if (err != EEXIST) - return err; - } + auto_ptr export_dir; + export_dir.reset(ExportDir::from_file_system(dir_name)); + if (!export_dir.get()) + return -EIO; DirHolder dh; - ret = dh.opendir(dir_name); + int ret = dh.opendir(dir_name); if (ret) { cerr << ERR_PREFIX << "opendir(" << dir_name << ") error: " << cpp_strerror(ret) << std::endl; @@ -937,7 +1012,7 @@ static int do_import(IoCtx& io_ctx, const char *dir_name, << "returned " << ret << std::endl; return ret; } - std::string obj_path(robj->get_fs_path(dir_name)); + std::string obj_path(robj->get_fs_path(export_dir.get())); auto_ptr lobj; ret = BackedUpObject::from_path(obj_path.c_str(), lobj); if (ret == ENOENT) { @@ -1036,36 +1111,9 @@ int rados_tool_sync(const std::map < std::string, std::string > &opts, std::string dir_name = (action == "import") ? src : dst; if (action == "import") { - if (access(dir_name.c_str(), R_OK)) { - cerr << "rados" << ": source directory '" << dst - << "' appears to be inaccessible." << std::endl; - exit(ENOENT); - } - ret = xattr_test(dir_name.c_str()); - if (ret) - return ret; return do_import(io_ctx, src.c_str(), force, delete_after); } else { - if (access(dst.c_str(), W_OK)) { - if (create) { - ret = mkdir(dst.c_str(), 0700); - if (ret < 0) { - ret = errno; - cerr << "rados" << ": mkdir(" << dst << ") failed with error " << ret - << std::endl; - exit(ret); - } - } - else { - cerr << "rados" << ": directory '" << dst << "' is not accessible. Use " - << "--create to try to create it.\n"; - exit(ENOENT); - } - } - ret = xattr_test(dir_name.c_str()); - if (ret) - return ret; - return do_export(io_ctx, dst.c_str(), force, delete_after); + return do_export(io_ctx, dst.c_str(), create, force, delete_after); } } diff --git a/src/test/test_rados_tool.sh b/src/test/test_rados_tool.sh index 83621506380cc..495fde550a7bd 100755 --- a/src/test/test_rados_tool.sh +++ b/src/test/test_rados_tool.sh @@ -57,22 +57,25 @@ TDIR=`mktemp -d -t test_rados_tool.XXXXXXXXXX` || die "mktemp failed" [ $KEEP_TEMP_FILES -eq 0 ] && trap "rm -rf ${TDIR}; exit" INT TERM EXIT mkdir "$TDIR/dira" -touch "$TDIR/dira/000029c4_foo" -attr -q -s rados_full_name -V "foo" "$TDIR/dira/000029c4_foo" -touch "$TDIR/dira/00003036_foo2" -attr -q -s rados_full_name -V "foo2" "$TDIR/dira/00003036_foo2" -touch "$TDIR/dira/000027d5_bar" -attr -q -s rados_full_name -V "bar" "$TDIR/dira/000027d5_bar" +attr -q -s "rados_sync_ver" -V "1" "$TDIR/dira" +touch "$TDIR/dira/foo" +attr -q -s "rados_full_name" -V "foo" "$TDIR/dira/foo" +touch "$TDIR/dira/foo2" +attr -q -s "rados_full_name" -V "foo2" "$TDIR/dira/foo2" +touch "$TDIR/dira/bar" +attr -q -s "rados_full_name" -V "bar" "$TDIR/dira/bar" mkdir "$TDIR/dirb" +attr -q -s "rados_sync_ver" -V "1" "$TDIR/dirb" mkdir "$TDIR/dirc" -touch "$TDIR/dirc/000029c4_foo" -attr -q -s rados_full_name -V "foo" "$TDIR/dirc/000029c4_foo" -attr -q -s "rados.toothbrush" -V "toothbrush" "$TDIR/dirc/000029c4_foo" -attr -q -s "rados.toothpaste" -V "crest" "$TDIR/dirc/000029c4_foo" -attr -q -s "rados.floss" -V "myfloss" "$TDIR/dirc/000029c4_foo" -touch "$TDIR/dirc/00003036_foo2" -attr -q -s "rados.toothbrush" -V "green" "$TDIR/dirc/00003036_foo2" -attr -q -s rados_full_name -V "foo2" "$TDIR/dirc/00003036_foo2" +attr -q -s "rados_sync_ver" -V "1" "$TDIR/dirc" +touch "$TDIR/dirc/foo" +attr -q -s "rados_full_name" -V "foo" "$TDIR/dirc/foo" +attr -q -s "rados.toothbrush" -V "toothbrush" "$TDIR/dirc/foo" +attr -q -s "rados.toothpaste" -V "crest" "$TDIR/dirc/foo" +attr -q -s "rados.floss" -V "myfloss" "$TDIR/dirc/foo" +touch "$TDIR/dirc/foo2" +attr -q -s "rados.toothbrush" -V "green" "$TDIR/dirc/foo2" +attr -q -s "rados_full_name" -V "foo2" "$TDIR/dirc/foo2" # make sure that --create works run "$RADOS_TOOL" rmpool "$POOL" @@ -108,8 +111,8 @@ run_expect_succ grep '\[force\]' "$TDIR/out2" run_expect_succ "$RADOS_TOOL" -C export "$POOL" "$TDIR/dirc_copy" # check to make sure extended attributes were preserved -PRE_EXPORT=`attr -qg rados.toothbrush "$TDIR/dirc/000029c4_foo"` -POST_EXPORT=`attr -qg rados.toothbrush "$TDIR/dirc_copy/000029c4_foo"` +PRE_EXPORT=`attr -qg user.rados.toothbrush "$TDIR/dirc/foo"` +POST_EXPORT=`attr -qg user.rados.toothbrush "$TDIR/dirc_copy/foo"` if [ "$PRE_EXPORT" != "$POST_EXPORT" ]; then die "xattr not preserved across import/export! \ \$PRE_EXPORT = $PRE_EXPORT, \$POST_EXPORT = $POST_EXPORT" @@ -117,14 +120,14 @@ fi # trigger a rados delete using --delete-after run_expect_succ "$RADOS_TOOL" --create export "$POOL" "$TDIR/dird" -rm -f "$TDIR/dird/000029c4_foo" +rm -f "$TDIR/dird/foo" run_expect_succ "$RADOS_TOOL" --delete-after import "$TDIR/dird" "$POOL" | tee "$TDIR/out3" run_expect_succ grep '\[deleted\]' "$TDIR/out3" # trigger a local delete using --delete-after run_expect_succ "$RADOS_TOOL" --delete-after export "$POOL" "$TDIR/dirc" | tee "$TDIR/out4" run_expect_succ grep '\[deleted\]' "$TDIR/out4" -[ -e "$TDIR/dird/000029c4_foo" ] && die "--delete-after failed to delete a file!" +[ -e "$TDIR/dird/foo" ] && die "--delete-after failed to delete a file!" echo "SUCCESS!" exit 0 -- 2.39.5