Set up bind mounts for a client when mounting it.

Summary:
This adds a new API to `PrivHelper`: `privilegedBindMount()`.
Similar to `privilegedFuseMount()`, this sends a message to the privileged helper,
which is running as `root`, so it can set up the specified bind mount.
The changes in the `privhelper` directory parrot what was done to support `privilegedFuseMount()`.

Now, once the primary mount for a client is created, any bind mounts listed in the
config for the client are set up. This logic is introduced in `EdenServer.cpp`.

Reviewed By: simpkins

Differential Revision: D3296660

fbshipit-source-id: 61296f35e5c3a6f232a1c17e0f296dd5d3b5ec06
This commit is contained in:
Michael Bolin 2016-05-23 21:32:12 -07:00 committed by Facebook Github Bot 8
parent 20ce44db52
commit d6d5d6c695
15 changed files with 331 additions and 62 deletions

View File

@ -27,6 +27,7 @@ def create_eden_fs_rules(suffix, subdir, server_srcs, server_deps):
":thrift-cpp2",
"@/common/fb303/cpp:fb303",
"@/eden/fuse:fusell",
"@/eden/fuse/privhelper:privhelper",
"@/eden/fs/config:config",
"@/eden/fs/inodes:inodes",
"@/folly/experimental:experimental",

View File

@ -9,18 +9,21 @@
*/
#include "EdenServer.h"
#include <boost/filesystem/operations.hpp>
#include <boost/filesystem/path.hpp>
#include <folly/SocketAddress.h>
#include <folly/String.h>
#include <gflags/gflags.h>
#include <thrift/lib/cpp2/server/ThriftServer.h>
#include <wangle/concurrent/CPUThreadPoolExecutor.h>
#include <wangle/concurrent/GlobalExecutor.h>
#include <boost/filesystem/path.hpp>
#include "EdenServiceHandler.h"
#include "eden/fs/config/ClientConfig.h"
#include "eden/fs/inodes/EdenMount.h"
#include "eden/fs/store/LocalStore.h"
#include "eden/fuse/MountPoint.h"
#include "eden/fuse/privhelper/PrivHelper.h"
DEFINE_bool(debug, false, "run fuse in debug mode");
@ -72,7 +75,9 @@ void EdenServer::run() {
runThriftServer();
}
void EdenServer::mount(std::shared_ptr<EdenMount> edenMount) {
void EdenServer::mount(
std::shared_ptr<EdenMount> edenMount,
std::unique_ptr<ClientConfig> config) {
// Add the mount point to mountPoints_.
// This also makes sure we don't have this path mounted already
auto mountPath = edenMount->getPath().stringPiece();
@ -98,6 +103,26 @@ void EdenServer::mount(std::shared_ptr<EdenMount> edenMount) {
this->mountFinished(edenMount.get());
throw;
}
// Perform all of the bind mounts associated with the client.
for (auto bindMount : config->getBindMounts()) {
auto pathInMountDir = bindMount.pathInMountDir;
try {
// If pathInMountDir does not exist, then it must be created before the
// bind mount is performed.
boost::system::error_code errorCode;
boost::filesystem::path mountDir = pathInMountDir.c_str();
boost::filesystem::create_directories(mountDir, errorCode);
fusell::privilegedBindMount(
bindMount.pathInClientDir.c_str(), pathInMountDir.c_str());
} catch (...) {
// Consider recording all failed bind mounts in a way that can be
// communicated back to the caller in a structured way.
LOG(ERROR) << "Failed to perform bind mount for "
<< pathInMountDir.stringPiece() << ".";
}
}
}
void EdenServer::mountFinished(EdenMount* edenMount) {

View File

@ -26,6 +26,7 @@ class ThriftServer;
namespace facebook {
namespace eden {
class ClientConfig;
class EdenMount;
class EdenServiceHandler;
class LocalStore;
@ -47,7 +48,10 @@ class EdenServer {
void run();
void mount(std::shared_ptr<EdenMount> edenMount);
void mount(
std::shared_ptr<EdenMount> edenMount,
std::unique_ptr<ClientConfig> config);
void unmount(folly::StringPiece mountPath);
const std::shared_ptr<EdenServiceHandler>& getHandler() const {

View File

@ -57,7 +57,7 @@ void EdenServiceHandler::mountImpl(const MountInfo& info) {
// TODO(mbolin): Use the result of config.getBindMounts() to perform the
// appropriate bind mounts for the client.
server_->mount(std::move(edenMount));
server_->mount(std::move(edenMount), std::move(config));
}
void EdenServiceHandler::mount(std::unique_ptr<MountInfo> info) {

View File

@ -157,10 +157,20 @@ folly::File privilegedFuseMount(folly::StringPiece mountPath) {
folly::File file;
gPrivHelper->sendAndRecv(&msg, &file);
PrivHelperConn::parseMountResponse(&msg);
PrivHelperConn::parseEmptyResponse(&msg);
CHECK(file) << "no file descriptor received in privhelper mount response";
return file;
}
void privilegedBindMount(
folly::StringPiece clientPath,
folly::StringPiece mountPath) {
PrivHelperConn::Message msg;
PrivHelperConn::serializeBindMountRequest(&msg, clientPath, mountPath);
gPrivHelper->sendAndRecv(&msg, nullptr);
PrivHelperConn::parseEmptyResponse(&msg);
}
}
}
} // facebook::eden::fusell

View File

@ -73,6 +73,15 @@ void dropPrivileges();
* itself will just pick the right values.
*/
folly::File privilegedFuseMount(folly::StringPiece mountPath);
/*
* @param clientPath Absolute path (that should be under
* .eden/clients/<client-name>/bind-mounts/) where the "real" storage is.
* @param mountPath Absolute path where the bind mount should be applied.
*/
void privilegedBindMount(
folly::StringPiece clientPath,
folly::StringPiece mountPath);
}
}
} // facebook::eden::fusell

View File

@ -264,7 +264,7 @@ void PrivHelperConn::recvMsg(Message* msg, folly::File* f) {
void PrivHelperConn::serializeMountRequest(
Message* msg,
StringPiece mountPoint) {
msg->msgType = REQ_MOUNT;
msg->msgType = REQ_MOUNT_FUSE;
IOBuf buf{IOBuf::WRAP_BUFFER, msg->data, sizeof(msg->data)};
buf.clear(); // Mark all the buffer space as unused
Appender a{&buf, 0};
@ -276,7 +276,7 @@ void PrivHelperConn::serializeMountRequest(
}
void PrivHelperConn::parseMountRequest(Message* msg, string& mountPoint) {
CHECK_EQ(msg->msgType, REQ_MOUNT);
CHECK_EQ(msg->msgType, REQ_MOUNT_FUSE);
CHECK_LE(msg->dataSize, sizeof(msg->data));
IOBuf buf{IOBuf::WRAP_BUFFER, msg->data, msg->dataSize};
@ -286,20 +286,54 @@ void PrivHelperConn::parseMountRequest(Message* msg, string& mountPoint) {
mountPoint = c.readFixedString(size);
}
void PrivHelperConn::serializeMountResponse(Message* msg) {
msg->msgType = RESP_MOUNT;
void PrivHelperConn::serializeEmptyResponse(Message* msg) {
msg->msgType = RESP_EMPTY;
msg->dataSize = 0;
}
void PrivHelperConn::parseMountResponse(const Message* msg) {
void PrivHelperConn::parseEmptyResponse(const Message* msg) {
if (msg->msgType == RESP_ERROR) {
rethrowErrorResponse(msg);
} else if (msg->msgType != RESP_MOUNT) {
} else if (msg->msgType != RESP_EMPTY) {
throw std::runtime_error(
folly::to<string>("unexpected response type: ", msg->msgType));
}
}
void PrivHelperConn::serializeBindMountRequest(
Message* msg,
folly::StringPiece clientPath,
folly::StringPiece mountPath) {
msg->msgType = REQ_MOUNT_BIND;
IOBuf buf{IOBuf::WRAP_BUFFER, msg->data, sizeof(msg->data)};
buf.clear(); // Mark all the buffer space as unused
Appender a{&buf, 0};
a.writeBE<uint32_t>(clientPath.size());
a.push(ByteRange(clientPath));
a.writeBE<uint32_t>(mountPath.size());
a.push(ByteRange(mountPath));
msg->dataSize = buf.length();
}
void PrivHelperConn::parseBindMountRequest(
Message* msg,
std::string& clientPath,
std::string& mountPath) {
CHECK_EQ(msg->msgType, REQ_MOUNT_BIND);
CHECK_LE(msg->dataSize, sizeof(msg->data));
IOBuf buf{IOBuf::WRAP_BUFFER, msg->data, msg->dataSize};
Cursor c{&buf};
auto clientPathSize = c.readBE<uint32_t>();
clientPath = c.readFixedString(clientPathSize);
auto mountPathSize = c.readBE<uint32_t>();
mountPath = c.readFixedString(mountPathSize);
}
void PrivHelperConn::serializeErrorResponse(
Message* msg,
const std::exception& ex) {

View File

@ -39,8 +39,9 @@ class PrivHelperConn {
enum MsgType : uint32_t {
MSG_TYPE_NONE = 0,
RESP_ERROR = 1,
REQ_MOUNT = 2,
RESP_MOUNT = 3,
RESP_EMPTY = 2,
REQ_MOUNT_FUSE = 3,
REQ_MOUNT_BIND = 4,
};
struct Message {
@ -123,10 +124,22 @@ class PrivHelperConn {
folly::StringPiece mountPoint);
static void parseMountRequest(Message* msg, std::string& mountPoint);
static void serializeMountResponse(Message* msg);
// Parse a mount response.
// Will throw an exception if this is actually an error response.
static void parseMountResponse(const Message* msg);
static void serializeBindMountRequest(
Message* msg,
folly::StringPiece clientPath,
folly::StringPiece mountPath);
static void parseBindMountRequest(
Message* msg,
std::string& clientPath,
std::string& mountPath);
static void serializeEmptyResponse(Message* msg);
/**
* Parse a response that is expected to be empty.
* Will throw an exception if this is actually an error response.
*/
static void parseEmptyResponse(const Message* msg);
static void serializeErrorResponse(Message* msg, const std::exception& ex);
static void serializeErrorResponse(

View File

@ -9,6 +9,7 @@
*/
#include "PrivHelperServer.h"
#include <boost/algorithm/string/predicate.hpp>
#include <fcntl.h>
#include <folly/Exception.h>
#include <folly/File.h>
@ -19,6 +20,7 @@
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/vfs.h>
#include <unistd.h>
#include <set>
@ -87,6 +89,14 @@ folly::File PrivHelperServer::fuseMount(const char* mountPath) {
return fuseDev;
}
void PrivHelperServer::bindMount(
const char* clientPath,
const char* mountPath) {
int rc = mount(
clientPath, mountPath, /* type */ nullptr, MS_BIND, /* data */ nullptr);
checkUnixError(rc, "failed to mount");
}
void PrivHelperServer::fuseUnmount(const char* mountPath) {
auto rc = umount2(mountPath, UMOUNT_NOFOLLOW);
if (rc != 0) {
@ -109,7 +119,7 @@ void PrivHelperServer::processMountMsg(PrivHelperConn::Message* msg) {
try {
fuseDev = fuseMount(mountPath.c_str());
mountPoints_.insert(mountPath);
conn_.serializeMountResponse(msg);
conn_.serializeEmptyResponse(msg);
} catch (const std::exception& ex) {
// Note that we re-use the request message buffer for the response data
conn_.serializeErrorResponse(msg, ex);
@ -121,13 +131,50 @@ void PrivHelperServer::processMountMsg(PrivHelperConn::Message* msg) {
conn_.sendMsg(msg, fuseDev.fd());
}
void PrivHelperServer::processBindMountMsg(PrivHelperConn::Message* msg) {
string clientPath;
string mountPath;
conn_.parseBindMountRequest(msg, clientPath, mountPath);
// Figure out which FUSE mount the mountPath belongs to.
// (Alternatively, we could just make this part of the Message.)
string key;
for (const auto& mountPoint : mountPoints_) {
if (boost::starts_with(mountPath, mountPoint + "/")) {
key = mountPoint;
break;
}
}
if (key.empty()) {
throw std::domain_error(
folly::to<string>("No FUSE mount found for ", mountPath));
}
try {
bindMount(clientPath.c_str(), mountPath.c_str());
bindMountPoints_.insert({key, mountPath});
conn_.serializeEmptyResponse(msg);
} catch (const std::exception& ex) {
// Note that we re-use the request message buffer for the response data
conn_.serializeErrorResponse(msg, ex);
conn_.sendMsg(msg);
return;
}
// Note that we re-use the request message buffer for the response data
conn_.sendMsg(msg);
}
void PrivHelperServer::messageLoop() {
PrivHelperConn::Message msg;
while (1) {
conn_.recvMsg(&msg, nullptr);
if (msg.msgType == PrivHelperConn::REQ_MOUNT) {
auto msgType = msg.msgType;
if (msgType == PrivHelperConn::REQ_MOUNT_FUSE) {
processMountMsg(&msg);
} else if (msgType == PrivHelperConn::REQ_MOUNT_BIND) {
processBindMountMsg(&msg);
} else {
// This shouldn't ever happen unless we have a bug.
// Crash if it does occur. (We could send back an error message and
@ -139,12 +186,47 @@ void PrivHelperServer::messageLoop() {
}
void PrivHelperServer::cleanupMountPoints() {
for (const auto& mp : mountPoints_) {
fuseUnmount(mp.c_str());
int numBindMountsRemoved = 0;
for (const auto& mountPoint : mountPoints_) {
// Clean up the bind mounts for a FUSE mount before the FUSE mount itself.
auto range = bindMountPoints_.equal_range(mountPoint);
for (auto it = range.first; it != range.second; ++it) {
auto bindMount = it->second;
auto path = bindMount.c_str();
bindUnmount(bindMount.c_str());
numBindMountsRemoved++;
}
// This appears to fail sometimes with "Device or resource busy" if a
// terminal is still open with the mountPoint as the working directory.
fuseUnmount(mountPoint.c_str());
}
CHECK_EQ(bindMountPoints_.size(), numBindMountsRemoved)
<< "All bind mounts should have been removed.";
bindMountPoints_.clear();
mountPoints_.clear();
}
void PrivHelperServer::bindUnmount(const char* mountPath) {
fuseUnmount(mountPath);
// Empirically, the unmount may not be complete when umount2() returns.
// To work around this, we repeatedly invoke statfs on the bind mount
// until it fails, demonstrating that it has finished unmounting.
struct statfs st;
int rc;
while (true) {
// This should have a non-zero exit code once the path is unmounted.
rc = statfs(mountPath, &st);
if (rc == 0) {
sched_yield();
} else {
break;
}
}
}
void PrivHelperServer::run() {
// Ignore SIGINT and SIGTERM.
// We should only exit when our parent process does.

View File

@ -13,6 +13,7 @@
#include <limits>
#include <set>
#include <string>
#include <unordered_map>
#include "PrivHelperConn.h"
namespace folly {
@ -46,10 +47,14 @@ class PrivHelperServer {
void messageLoop();
void cleanupMountPoints();
void processMountMsg(PrivHelperConn::Message* msg);
void processBindMountMsg(PrivHelperConn::Message* msg);
// These methods are virtual so we can override them during unit tests
virtual folly::File fuseMount(const char* mountPath);
virtual void fuseUnmount(const char* mountPath);
// Both clientPath and mountPath must be existing directories.
virtual void bindMount(const char* clientPath, const char* mountPath);
virtual void bindUnmount(const char* mountPath);
PrivHelperConn conn_;
uid_t uid_{std::numeric_limits<uid_t>::max()};
@ -58,6 +63,7 @@ class PrivHelperServer {
// The privhelper server only has a single thread,
// so we don't need to lock the following state
std::set<std::string> mountPoints_;
std::unordered_multimap<std::string, std::string> bindMountPoints_;
};
}
}

View File

@ -6,4 +6,7 @@ cpp_library(
"@/folly/io:iobuf",
"@/folly:folly",
],
external_deps = [
("boost", None, "boost_filesystem"),
],
)

View File

@ -11,8 +11,10 @@
#include "eden/fuse/privhelper/PrivHelper.h"
#include "eden/fuse/privhelper/PrivHelperConn.h"
#include <boost/filesystem.hpp>
#include <folly/Exception.h>
#include <folly/File.h>
#include <folly/FileUtil.h>
#include <folly/Range.h>
#include <folly/experimental/TestUtil.h>
#include <folly/io/IOBuf.h>
@ -206,8 +208,8 @@ TEST(PrivHelper, SerializeError) {
// Try parsing it as a mount response
try {
PrivHelperConn::parseMountResponse(&msg);
FAIL() << "expected parseMountResponse() to throw";
PrivHelperConn::parseEmptyResponse(&msg);
FAIL() << "expected parseEmptyResponse() to throw";
} catch (const std::system_error& ex) {
EXPECT_EQ(std::system_category(), ex.code().category());
EXPECT_EQ(ENOENT, ex.code().value());
@ -218,7 +220,23 @@ TEST(PrivHelper, SerializeError) {
TEST(PrivHelper, ServerShutdownTest) {
TemporaryDirectory tmpDir;
PrivHelperTestServer server(tmpDir.path().string());
PrivHelperTestServer server;
auto fooDir = tmpDir.path() / "foo";
create_directory(fooDir);
auto foo = fooDir.string();
// Note we do not create this directory explicitly because we want to verify
// that privilegedBindMount takes care of this for us.
auto mountedBuckOut = tmpDir.path() / "foo" / "buck-out";
auto barDir = tmpDir.path() / "bar";
create_directory(barDir);
auto bar = barDir.string();
auto otherDir = (tmpDir.path() / "other");
create_directory(barDir);
auto other = otherDir.string();
{
startPrivHelper(&server, getuid(), getgid());
@ -227,17 +245,27 @@ TEST(PrivHelper, ServerShutdownTest) {
};
// Create a few mount points
auto foo = privilegedFuseMount("foo");
auto bar = privilegedFuseMount("bar");
EXPECT_TRUE(server.isMounted("foo"));
EXPECT_TRUE(server.isMounted("bar"));
EXPECT_FALSE(server.isMounted("other"));
privilegedFuseMount(foo);
privilegedFuseMount(bar);
EXPECT_TRUE(server.isMounted(foo));
EXPECT_TRUE(server.isMounted(bar));
EXPECT_FALSE(server.isMounted(other));
// Create a bind mount.
EXPECT_FALSE(boost::filesystem::exists(mountedBuckOut));
TemporaryDirectory realBuckOut;
privilegedBindMount(realBuckOut.path().c_str(), mountedBuckOut.c_str());
EXPECT_TRUE(server.isBindMounted(mountedBuckOut.c_str()));
EXPECT_TRUE(boost::filesystem::exists(mountedBuckOut))
<< "privilegedBindMount() should create the bind mount directory for "
"the caller.";
// The privhelper will exit at the end of this scope
}
// Make sure things get umounted when the privhelper quits
EXPECT_FALSE(server.isMounted("foo"));
EXPECT_FALSE(server.isMounted("bar"));
EXPECT_FALSE(server.isMounted("other"));
EXPECT_FALSE(server.isMounted(foo));
EXPECT_FALSE(server.isBindMounted(mountedBuckOut.string()));
EXPECT_FALSE(server.isMounted(bar));
EXPECT_FALSE(server.isMounted(other));
}

View File

@ -9,45 +9,88 @@
*/
#include "PrivHelperTestServer.h"
#include <boost/filesystem.hpp>
#include <folly/File.h>
#include <folly/FileUtil.h>
#include <system_error>
using folly::File;
using folly::StringPiece;
using std::string;
namespace facebook {
namespace eden {
namespace fusell {
PrivHelperTestServer::PrivHelperTestServer(StringPiece tmpDir)
: tmpDir_(tmpDir.str()) {}
PrivHelperTestServer::PrivHelperTestServer() {}
// FUSE mounts.
File PrivHelperTestServer::fuseMount(const char* mountPath) {
// Just open a new file inside our temporary directory,
// and write "mounted" into it.
File f(getMountPath(mountPath).c_str(), O_RDWR | O_CREAT | O_TRUNC);
// Create a single file named "mounted" and write "mounted" into it.
auto pathToNewFile = getPathToMountMarker(mountPath);
File f(pathToNewFile, O_RDWR | O_CREAT | O_TRUNC);
StringPiece data{"mounted"};
folly::writeFull(f.fd(), data.data(), data.size());
return f;
}
void PrivHelperTestServer::fuseUnmount(const char* mountPath) {
// Replace the file contents with "unmounted"
File f(getMountPath(mountPath).c_str(), O_RDWR | O_CREAT | O_TRUNC);
StringPiece data{"unmounted"};
folly::writeFull(f.fd(), data.data(), data.size());
}
std::string PrivHelperTestServer::getMountPath(StringPiece mountPath) const {
return tmpDir_ + "/" + mountPath.str();
// Replace the file contents with "unmounted".
folly::writeFile(
StringPiece{"unmounted"}, getPathToMountMarker(mountPath).c_str());
}
bool PrivHelperTestServer::isMounted(folly::StringPiece mountPath) const {
return checkIfMarkerFileHasContents(
getPathToMountMarker(mountPath), "mounted");
}
string PrivHelperTestServer::getPathToMountMarker(StringPiece mountPath) const {
return mountPath.str() + "/mounted";
}
// Bind mounts.
void PrivHelperTestServer::bindMount(
const char* clientPath,
const char* mountPath) {
// Create a single file named "bind-mounted" and write "bind-mounted" into it.
// Normally, the caller to the PrivHelper (in practice, EdenServer) is
// responsible for creating the directory before requesting the bind mount.
boost::filesystem::create_directories(mountPath);
auto fileInMountPath = getPathToBindMountMarker(mountPath);
folly::writeFile(StringPiece{"bind-mounted"}, fileInMountPath.c_str());
}
void PrivHelperTestServer::bindUnmount(const char* mountPath) {
// Replace the file contents with "bind-unmounted".
folly::writeFile(
StringPiece{"bind-unmounted"},
getPathToBindMountMarker(mountPath).c_str());
}
bool PrivHelperTestServer::isBindMounted(folly::StringPiece mountPath) const {
return checkIfMarkerFileHasContents(
getPathToBindMountMarker(mountPath), "bind-mounted");
}
string PrivHelperTestServer::getPathToBindMountMarker(
StringPiece mountPath) const {
return mountPath.str() + "/bind-mounted";
}
// General helpers.
bool PrivHelperTestServer::checkIfMarkerFileHasContents(
const string pathToMarkerFile,
const string contents) const {
try {
std::string data;
folly::readFile(getMountPath(mountPath).c_str(), data, 256);
return data == "mounted";
string data;
folly::readFile(pathToMarkerFile.c_str(), data, 256);
return data == contents;
} catch (const std::system_error& ex) {
if (ex.code().category() == std::system_category() &&
ex.code().value() == ENOENT) {

View File

@ -24,12 +24,7 @@ namespace fusell {
*/
class PrivHelperTestServer : public PrivHelperServer {
public:
explicit PrivHelperTestServer(folly::StringPiece tmpDir);
/*
* Get the path to the test file representing the given mount point.
*/
std::string getMountPath(folly::StringPiece mountPath) const;
PrivHelperTestServer();
/*
* Check if the given mount point is mounted.
@ -39,11 +34,24 @@ class PrivHelperTestServer : public PrivHelperServer {
*/
bool isMounted(folly::StringPiece mountPath) const;
/**
* Check if the given path is bind mounted.
*/
bool isBindMounted(folly::StringPiece mountPath) const;
private:
folly::File fuseMount(const char* mountPath) override;
void fuseUnmount(const char* mountPath) override;
std::string getPathToMountMarker(folly::StringPiece mountPath) const;
std::string tmpDir_;
void bindMount(const char* clientPath, const char* mountPath) override;
void bindUnmount(const char* mountPath) override;
std::string getPathToBindMountMarker(folly::StringPiece mountPath) const;
/** @return true if the marker file exists with the specified contents. */
bool checkIfMarkerFileHasContents(
const std::string pathToMarkerFile,
const std::string contents) const;
};
}
}

View File

@ -1,9 +1,12 @@
cpp_unittest(
name = "test",
srcs = glob(["*.cpp"]),
headers = glob(["*.h"]),
deps = [
"@/eden/fuse/privhelper:privhelper",
"@/folly/experimental:test_util",
],
name = 'test',
srcs = glob(['*.cpp']),
headers = glob(['*.h']),
deps = [
'@/eden/fuse/privhelper:privhelper',
'@/folly/experimental:test_util',
],
external_deps = [
('boost', 'any'),
],
)