nfs: implement the CREATE RPC

Summary:
The CREATE RPC is intended to create regular files on the server. It supports 3
ways of creating them: unchecked, guarded and exclusive. The exclusive mode
allows for a client to being able to re-transmit the creation and for it to not
fail when that happens. The way this is implemented requires the server to
store a cookie passed in by the client and on retransmission to compare it in
order to not fail the file creation. This complexity may not be warranted and
needed in the case of EdenFS, and the spec allows for a server to not support
this operation, thus EdenFS claims to not support it.

The 2 other modes are more traditional, guarded will simply fail if the file
already exist, while unchecked succeed in that same situation. It's not
entirely clear in the RFC what the behavior is supposed to be in the unchecked
case, and whether the attributes needs to be applied. For now, the error is
just ignored, a warning message is logged and a success is returned in this
case. In the future, setting the file attribute (in particular the mode and
size) may be necessary.

Reviewed By: kmancini

Differential Revision: D26654766

fbshipit-source-id: c2e8142b8a4ff756a868e5859fdda4b07e53bddf
This commit is contained in:
Xavier Deguillard 2021-03-04 09:57:01 -08:00 committed by Facebook GitHub Bot
parent 2901ea4c1d
commit b46a7b2a11
4 changed files with 150 additions and 8 deletions

View File

@ -65,6 +65,31 @@ folly::Future<std::string> NfsDispatcherImpl::readlink(
});
}
folly::Future<NfsDispatcher::CreateRes> NfsDispatcherImpl::create(
InodeNumber dir,
PathComponent name,
mode_t mode,
ObjectFetchContext& context) {
// Make sure that we're attempting to create a file.
mode = S_IFREG | (0777 & mode);
return inodeMap_->lookupTreeInode(dir).thenValue(
[&context, name = std::move(name), mode](const TreeInodePtr& inode) {
// TODO(xavierd): Modify mknod to obtain the pre and post stat of the
// directory.
// Set dev to 0 as this is unused for a regular file.
auto newFile = inode->mknod(name, mode, 0, InvalidationRequired::No);
auto statFut = newFile->stat(context);
return std::move(statFut).thenValue(
[newFile = std::move(newFile)](struct stat&& stat) {
return CreateRes{
newFile->getNodeId(),
std::move(stat),
std::nullopt,
std::nullopt};
});
});
}
folly::Future<NfsDispatcher::MkdirRes> NfsDispatcherImpl::mkdir(
InodeNumber dir,
PathComponent name,

View File

@ -36,6 +36,12 @@ class NfsDispatcherImpl : public NfsDispatcher {
InodeNumber ino,
ObjectFetchContext& context) override;
folly::Future<NfsDispatcher::CreateRes> create(
InodeNumber ino,
PathComponent name,
mode_t mode,
ObjectFetchContext& context) override;
folly::Future<NfsDispatcher::MkdirRes> mkdir(
InodeNumber ino,
PathComponent name,

View File

@ -64,6 +64,36 @@ class NfsDispatcher {
InodeNumber ino,
ObjectFetchContext& context) = 0;
/**
* Return value of the create method.
*/
struct CreateRes {
/** InodeNumber of the created file */
InodeNumber ino;
/** Attributes of the created file */
struct stat stat;
/** Attributes of the directory prior to creating the file */
std::optional<struct stat> preDirStat;
/** Attributes of the directory after creating the file */
std::optional<struct stat> postDirStat;
};
/**
* Create a regular file in the directory referenced by the InodeNumber dir.
*
* Both the pre and post stat for that directory needs to be collected in an
* atomic manner: no other operation on the directory needs to be allowed in
* between them. This is to ensure that the NFS client can properly detect if
* its cache needs to be invalidated. Setting them both to std::nullopt is an
* acceptable approach if the stat cannot be collected atomically.
*/
virtual folly::Future<CreateRes> create(
InodeNumber dir,
PathComponent name,
mode_t mode,
ObjectFetchContext& context) = 0;
/**
* Return value of the mkdir method.
*/
@ -82,11 +112,8 @@ class NfsDispatcher {
/**
* Create a subdirectory in the directory referenced by the InodeNumber dir.
*
* Both the pre and post stat for that directory needs to be collected in an
* atomic manner: no other operation on the directory needs to be allowed in
* between them. This is to ensure that the NFS client can properly detect if
* its cache needs to be invalidated. Setting them both to std::nullopt is an
* acceptable approach if the stat cannot be collected atomically.
* For the pre and post dir stat, refer to the documentation of the create
* method above.
*/
virtual folly::Future<MkdirRes> mkdir(
InodeNumber dir,

View File

@ -481,12 +481,96 @@ folly::Future<folly::Unit> Nfsd3ServerProcessor::write(
return folly::unit;
}
/**
* Test if the exception was raised due to a EEXIST condition.
*/
bool isEexist(const folly::exception_wrapper& ex) {
if (auto* err = ex.get_exception<std::system_error>()) {
return isErrnoError(*err) && err->code().value() == EEXIST;
}
return false;
}
folly::Future<folly::Unit> Nfsd3ServerProcessor::create(
folly::io::Cursor /*deser*/,
folly::io::Cursor deser,
folly::io::Appender ser,
uint32_t xid) {
serializeReply(ser, accept_stat::PROC_UNAVAIL, xid);
return folly::unit;
serializeReply(ser, accept_stat::SUCCESS, xid);
auto args = XdrTrait<CREATE3args>::deserialize(deser);
static auto context =
ObjectFetchContext::getNullContextWithCauseDetail("create");
if (args.how.tag == createmode3::EXCLUSIVE) {
// Exclusive file creation is complicated, for now let's not support it.
CREATE3res res{{{nfsstat3::NFS3ERR_NOTSUPP, CREATE3resfail{wcc_data{}}}}};
XdrTrait<CREATE3res>::serialize(ser, res);
return folly::unit;
}
auto& attr = std::get<sattr3>(args.how.v);
// If the mode isn't set, make it writable by the owner, readable by the
// group and other. This is consistent with creating a file with a default
// umask of 022.
auto mode =
attr.mode.tag ? std::get<uint32_t>(attr.mode.v) : (S_IFREG | 0644);
return dispatcher_
->create(
args.where.dir.ino, PathComponent{args.where.name}, mode, *context)
.thenTry([ser = std::move(ser), createmode = args.how.tag](
folly::Try<NfsDispatcher::CreateRes> try_) mutable {
if (try_.hasException()) {
if (createmode == createmode3::UNCHECKED &&
isEexist(try_.exception())) {
XLOG(WARN) << "Unchecked file creation returned EEXIST";
// A file already exist at that location, since this is an
// UNCHECKED creation, just pretend the file was created just fine.
// Since no fields are populated, this forces the client to issue a
// LOOKUP RPC to gather the InodeNumber and attributes for this
// file. This is probably fine as creating a file that already
// exists should be a rare event.
// TODO(xavierd): We should change the file attributes based on
// the requested args.how.obj_attributes.
CREATE3res res{
{{nfsstat3::NFS3_OK,
CREATE3resok{
/*obj*/ post_op_fh3{},
/*obj_attributes*/ post_op_attr{},
wcc_data{
/*before*/ pre_op_attr{},
/*after*/ post_op_attr{},
}}}}};
XdrTrait<CREATE3res>::serialize(ser, res);
} else {
CREATE3res res{
{{exceptionToNfsError(try_.exception()), CREATE3resfail{}}}};
XdrTrait<CREATE3res>::serialize(ser, res);
}
} else {
auto createRes = std::move(try_).value();
CREATE3res res{
{{nfsstat3::NFS3_OK,
CREATE3resok{
/*obj*/ post_op_fh3{nfs_fh3{createRes.ino}},
/*obj_attributes*/
post_op_attr{statToFattr3(createRes.stat)},
wcc_data{
/*before*/ createRes.preDirStat.has_value()
? statToPreOpAttr(createRes.preDirStat.value())
: pre_op_attr{},
/*after*/ createRes.postDirStat.has_value()
? post_op_attr{statToFattr3(
createRes.postDirStat.value())}
: post_op_attr{},
}}}}};
XdrTrait<CREATE3res>::serialize(ser, res);
}
return folly::unit;
});
}
folly::Future<folly::Unit> Nfsd3ServerProcessor::mkdir(