From: Richard Weinberger <richard@nod.at>
To: linux-nfs@vger.kernel.org
Cc: david@sigma-star.at, bfields@fieldses.org,
luis.turcitu@appsbroker.com, david.young@appsbroker.com,
david.oberhollenzer@sigma-star.at,
trond.myklebust@hammerspace.com, anna.schumaker@netapp.com,
chris.chilvers@appsbroker.com,
Richard Weinberger <richard@nod.at>
Subject: [RFC PATCH 1/6] Implement reexport helper library
Date: Thu, 17 Feb 2022 14:15:26 +0100 [thread overview]
Message-ID: <20220217131531.2890-2-richard@nod.at> (raw)
In-Reply-To: <20220217131531.2890-1-richard@nod.at>
This internal library contains code that will be used by various
tools within the nfs-utils package to deal better with NFS re-export,
especially cross mounts.
Signed-off-by: Richard Weinberger <richard@nod.at>
---
configure.ac | 12 +
support/Makefile.am | 4 +
support/reexport/Makefile.am | 6 +
support/reexport/reexport.c | 477 +++++++++++++++++++++++++++++++++++
support/reexport/reexport.h | 53 ++++
5 files changed, 552 insertions(+)
create mode 100644 support/reexport/Makefile.am
create mode 100644 support/reexport/reexport.c
create mode 100644 support/reexport/reexport.h
diff --git a/configure.ac b/configure.ac
index 93626d62..86bf8ba9 100644
--- a/configure.ac
+++ b/configure.ac
@@ -274,6 +274,17 @@ AC_ARG_ENABLE(nfsv4server,
fi
AM_CONDITIONAL(CONFIG_NFSV4SERVER, [test "$enable_nfsv4server" = "yes" ])
+AC_ARG_ENABLE(reexport,
+ [AC_HELP_STRING([--enable-reexport],
+ [enable support for re-exporting NFS mounts @<:@default=no@:>@])],
+ enable_reexport=$enableval,
+ enable_reexport="no")
+ if test "$enable_reexport" = yes; then
+ AC_DEFINE(HAVE_REEXPORT_SUPPORT, 1,
+ [Define this if you want NFS re-export support compiled in])
+ fi
+ AM_CONDITIONAL(CONFIG_REEXPORT, [test "$enable_reexport" = "yes" ])
+
dnl Check for TI-RPC library and headers
AC_LIBTIRPC
@@ -730,6 +741,7 @@ AC_CONFIG_FILES([
support/nsm/Makefile
support/nfsidmap/Makefile
support/nfsidmap/libnfsidmap.pc
+ support/reexport/Makefile
tools/Makefile
tools/locktest/Makefile
tools/nlmtest/Makefile
diff --git a/support/Makefile.am b/support/Makefile.am
index c962d4d4..986e9b5f 100644
--- a/support/Makefile.am
+++ b/support/Makefile.am
@@ -10,6 +10,10 @@ if CONFIG_JUNCTION
OPTDIRS += junction
endif
+if CONFIG_REEXPORT
+OPTDIRS += reexport
+endif
+
SUBDIRS = export include misc nfs nsm $(OPTDIRS)
MAINTAINERCLEANFILES = Makefile.in
diff --git a/support/reexport/Makefile.am b/support/reexport/Makefile.am
new file mode 100644
index 00000000..9d544a8f
--- /dev/null
+++ b/support/reexport/Makefile.am
@@ -0,0 +1,6 @@
+## Process this file with automake to produce Makefile.in
+
+noinst_LIBRARIES = libreexport.a
+libreexport_a_SOURCES = reexport.c
+
+MAINTAINERCLEANFILES = Makefile.in
diff --git a/support/reexport/reexport.c b/support/reexport/reexport.c
new file mode 100644
index 00000000..551ec278
--- /dev/null
+++ b/support/reexport/reexport.c
@@ -0,0 +1,477 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <assert.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <pthread.h>
+#include <sqlite3.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <sys/file.h>
+#include <sys/mman.h>
+#include <sys/shm.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/vfs.h>
+#include <unistd.h>
+
+#include "nfslib.h"
+#include "reexport.h"
+#include "xlog.h"
+
+#define REEXPDB_SHM_NAME "/nfs_reexport_db_lock"
+#define REEXPDB_SHM_SZ 4096
+#define REEXPDB_INIT_LOCK NFS_STATEDIR "/reexpdb_init.lock"
+#define REEXPDB_DBFILE NFS_STATEDIR "/reexpdb.sqlite3"
+
+static const char initdb_sql[] = "CREATE TABLE IF NOT EXISTS fsidnums (num INTEGER PRIMARY KEY CHECK (num > 0 AND num < 4294967296), path TEXT UNIQUE); CREATE TABLE IF NOT EXISTS subvolumes (path TEXT PRIMARY KEY); CREATE INDEX IF NOT EXISTS idx_ids_path ON fsidnums (path);";
+/*
+ * This query is a little tricky. We use SQL to find and claim the smallest free fsid number.
+ * To find a free fsid the fsidnums is left joined to itself but with an offset of 1.
+ * Everything after the UNION statement is to handle the corner case where fsidnums
+ * is empty. In this case we want 1 as first fsid number.
+ */
+static const char new_fsidnum_by_path_sql[] = "INSERT INTO fsidnums VALUES ((SELECT ids1.num + 1 FROM fsidnums AS ids1 LEFT JOIN fsidnums AS ids2 ON ids2.num = ids1.num + 1 WHERE ids2.num IS NULL UNION SELECT 1 WHERE NOT EXISTS (SELECT NULL FROM fsidnums WHERE num = 1) LIMIT 1), ?1) RETURNING num;";
+static const char fsidnum_by_path_sql[] = "SELECT num FROM fsidnums WHERE path = ?1;";
+static const char add_crossed_volume_sql[] = "REPLACE INTO subvolumes VALUES(?1);";
+static const char drop_crossed_volume_sql[] = "DELETE FROM subvolumes WHERE path = ?1;";
+static const char get_crossed_volumes_sql[] = "SELECT path from subvolumes;";
+
+static sqlite3 *db;
+static pthread_rwlock_t *reexpdb_rwlock;
+static int init_done;
+
+static void reexpdb_wrlock(void)
+{
+ assert(pthread_rwlock_wrlock(reexpdb_rwlock) == 0);
+}
+
+static void reexpdb_rdlock(void)
+{
+ assert(pthread_rwlock_rdlock(reexpdb_rwlock) == 0);
+}
+
+static void reexpdb_unlock(void)
+{
+ assert(pthread_rwlock_unlock(reexpdb_rwlock) == 0);
+}
+
+static int init_shm_lock(void)
+{
+ int lockfd = -1, shmfd = -1;
+ int initlock = 0;
+ int ret = 0;
+
+ assert(sizeof(*reexpdb_rwlock) <= REEXPDB_SHM_SZ);
+
+ lockfd = open(REEXPDB_INIT_LOCK, O_RDWR | O_CREAT, 0600);
+ if (lockfd == -1) {
+ ret = -1;
+ xlog(L_FATAL, "Unable to open %s: %m", REEXPDB_INIT_LOCK);
+
+ goto out;
+ }
+
+ ret = flock(lockfd, LOCK_EX);
+ if (ret == -1) {
+ ret = -1;
+ xlog(L_FATAL, "Unable to lock %s: %m", REEXPDB_INIT_LOCK);
+
+ goto out_close;
+ }
+
+ shmfd = shm_open(REEXPDB_SHM_NAME, O_RDWR, 0600);
+ if (shmfd == -1 && errno == ENOENT) {
+ shmfd = shm_open(REEXPDB_SHM_NAME, O_RDWR | O_CREAT, 0600);
+ if (shmfd == -1) {
+ ret = -1;
+ xlog(L_FATAL, "Unable to create shared memory: %m");
+ goto out_unflock;
+ }
+
+ ret = ftruncate(shmfd, REEXPDB_SHM_SZ);
+ if (ret == -1) {
+ ret = -1;
+ xlog(L_FATAL, "Unable to ftruncate shared memory: %m");
+ goto out_unflock;
+ }
+
+ initlock = 1;
+ } else if (shmfd == -1) {
+ ret = -1;
+ xlog(L_FATAL, "Unable to open shared memory: %m");
+ goto out_unflock;
+ }
+
+ reexpdb_rwlock = mmap(NULL, REEXPDB_SHM_SZ, PROT_READ | PROT_WRITE, MAP_SHARED, shmfd, 0);
+ close(shmfd);
+ if (reexpdb_rwlock == (void *)-1) {
+ xlog(L_FATAL, "Unable to mmap shared memory: %m");
+ ret = -1;
+ goto out_unflock;
+ }
+
+ if (initlock) {
+ pthread_rwlockattr_t attr;
+
+ ret = pthread_rwlockattr_init(&attr);
+ if (ret != 0) {
+ xlog(L_FATAL, "Unable to pthread_rwlockattr_init: %m");
+ ret = -1;
+ goto out_unflock;
+ }
+
+ ret = pthread_rwlockattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
+ if (ret != 0) {
+ xlog(L_FATAL, "Unable to set PTHREAD_PROCESS_SHARED: %m");
+ ret = -1;
+ goto out_unflock;
+ }
+
+ ret = pthread_rwlock_init(reexpdb_rwlock, &attr);
+ if (ret != 0) {
+ xlog(L_FATAL, "Unable to pthread_rwlock_init: %m");
+ ret = -1;
+ goto out_unflock;
+ }
+ }
+
+ ret = 0;
+
+out_unflock:
+ flock(lockfd, LOCK_UN);
+out_close:
+ close(lockfd);
+out:
+ return ret;
+}
+
+/*
+ * reexpdb_init - Initialize reexport database
+ *
+ * Setup shared lock (database is concurrently used by multiple processes),
+ * if needed create tables and create database handle.
+ * It is okay to call this function multiple times per process.
+ */
+int reexpdb_init(void)
+{
+ char *sqlerr;
+ int ret;
+
+ if (init_done)
+ return 0;
+
+ ret = init_shm_lock();
+ if (ret)
+ return -1;
+
+ ret = sqlite3_open_v2(REEXPDB_DBFILE, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, NULL);
+ if (ret != SQLITE_OK) {
+ xlog(L_ERROR, "Unable to open reexport database: %s", sqlite3_errstr(ret));
+ return -1;
+ }
+
+ reexpdb_wrlock();
+ ret = sqlite3_exec(db, initdb_sql, NULL, NULL, &sqlerr);
+ reexpdb_unlock();
+ if (ret != SQLITE_OK) {
+ xlog(L_ERROR, "Unable to init reexport database: %s", sqlite3_errstr(ret));
+ sqlite3_free(sqlerr);
+ sqlite3_close_v2(db);
+ ret = -1;
+ } else {
+ init_done = 1;
+ ret = 0;
+ }
+
+ return ret;
+}
+
+/*
+ * reexpdb_destroy - Undo reexpdb_init().
+ *
+ * The shared lock keeps. We cannot know which other
+ * processes are still use the database.
+ */
+void reexpdb_destroy(void)
+{
+ if (!init_done)
+ return;
+
+ sqlite3_close_v2(db);
+ munmap((void *)reexpdb_rwlock, REEXPDB_SHM_SZ);
+ reexpdb_rwlock = NULL;
+}
+
+static int get_fsidnum_by_path(char *path, uint32_t *fsidnum)
+{
+ sqlite3_stmt *stmt = NULL;
+ int found = 0;
+ int ret;
+
+ ret = sqlite3_prepare_v2(db, fsidnum_by_path_sql, sizeof(fsidnum_by_path_sql), &stmt, NULL);
+ if (ret != SQLITE_OK) {
+ xlog(L_WARNING, "Unable to prepare SQL query: %s", sqlite3_errstr(ret));
+ goto out;
+ }
+
+ ret = sqlite3_bind_text(stmt, 1, path, -1, NULL);
+ if (ret != SQLITE_OK) {
+ xlog(L_WARNING, "Unable to bind \"%s\" SQL query: %s", __func__, sqlite3_errstr(ret));
+ goto out;
+ }
+
+ reexpdb_rdlock();
+ ret = sqlite3_step(stmt);
+ if (ret == SQLITE_ROW) {
+ *fsidnum = sqlite3_column_int(stmt, 0);
+ found = 1;
+ } else if (ret == SQLITE_DONE) {
+ /* No hit */
+ found = 0;
+ } else {
+ xlog(L_WARNING, "Error while looking up \"%s\" in database: %s", path, sqlite3_errstr(ret));
+ }
+ reexpdb_unlock();
+
+out:
+ sqlite3_finalize(stmt);
+ return found;
+}
+
+static int new_fsidnum_by_path(char *path, uint32_t *fsidnum)
+{
+ sqlite3_stmt *stmt = NULL;
+ int found = 0, check = 0;
+ int ret;
+
+ ret = sqlite3_prepare_v2(db, new_fsidnum_by_path_sql, sizeof(new_fsidnum_by_path_sql), &stmt, NULL);
+ if (ret != SQLITE_OK) {
+ xlog(L_WARNING, "Unable to prepare SQL query: %s", sqlite3_errstr(ret));
+ goto out;
+ }
+
+ ret = sqlite3_bind_text(stmt, 1, path, -1, NULL);
+ if (ret != SQLITE_OK) {
+ xlog(L_WARNING, "Unable to bind \"%s\" SQL query: %s", path, sqlite3_errstr(ret));
+ goto out;
+ }
+
+ reexpdb_wrlock();
+ ret = sqlite3_step(stmt);
+ if (ret == SQLITE_ROW) {
+ *fsidnum = sqlite3_column_int(stmt, 0);
+ found = 1;
+ } else if (ret == SQLITE_CONSTRAINT) {
+ /* Maybe we lost the race against another writer and the path is now present. */
+ check = 1;
+ } else {
+ xlog(L_WARNING, "Error while looking up \"%s\" in database: %s", path, sqlite3_errstr(ret));
+ }
+ reexpdb_unlock();
+
+out:
+ sqlite3_finalize(stmt);
+
+ if (check) {
+ found = get_fsidnum_by_path(path, fsidnum);
+ if (!found)
+ xlog(L_WARNING, "SQLITE_CONSTRAINT error while inserting \"%s\" in database", path);
+ }
+
+ return found;
+}
+
+int reexpdb_fsidnum_by_path(char *path, uint32_t *fsidnum, int may_create)
+{
+ int found;
+
+ found = get_fsidnum_by_path(path, fsidnum);
+
+ if (!found && may_create)
+ found = new_fsidnum_by_path(path, fsidnum);
+
+ return found;
+}
+
+int reexpdb_apply_reexport_settings(struct exportent *ep, char *flname, int flline)
+{
+ int ret = 0;
+
+ switch (ep->e_reexport) {
+ case REEXP_REMOTE_DEVFSID:
+ if (!ep->e_fsid && !ep->e_uuid) {
+ xlog(L_ERROR, "%s:%i: Selected 'reexport=' mode needs either a numerical or UUID 'fsid='\n",
+ flname, flline);
+ ret = -1;
+ }
+ break;
+ case REEXP_AUTO_FSIDNUM:
+ case REEXP_PREDEFINED_FSIDNUM: {
+ uint32_t fsidnum;
+ int found;
+
+ if (ep->e_uuid)
+ break;
+
+ if (reexpdb_init() != 0) {
+ ret = -1;
+
+ break;
+ }
+
+ found = reexpdb_fsidnum_by_path(ep->e_path, &fsidnum, 0);
+ if (!found) {
+ if (ep->e_reexport == REEXP_AUTO_FSIDNUM) {
+ found = reexpdb_fsidnum_by_path(ep->e_path, &fsidnum, 1);
+ if (!found) {
+ xlog(L_ERROR, "%s:%i: Unable to generate fsid for %s",
+ flname, flline, ep->e_path);
+ ret = -1;
+
+ break;
+ }
+ } else {
+ if (!ep->e_fsid) {
+ xlog(L_ERROR, "%s:%i: Selected 'reexport=' mode requires either a UUID 'fsid=' or a numerical 'fsid=' or a reexport db entry %d",
+ flname, flline, ep->e_fsid);
+ ret = -1;
+ }
+
+ break;
+ }
+ }
+
+ if (ep->e_fsid) {
+ if (ep->e_fsid != fsidnum) {
+ xlog(L_ERROR, "%s:%i: Selected 'reexport=' mode requires configured numerical 'fsid=' to agree with reexport db entry",
+ flname, flline);
+ ret = -1;
+ }
+
+ break;
+ }
+
+ ep->e_fsid = fsidnum;
+
+ break;
+ }
+ }
+
+ return ret;
+}
+
+int reexpdb_add_subvolume(char *path)
+{
+ sqlite3_stmt *stmt = NULL;
+ int ret;
+
+ reexpdb_wrlock();
+ ret = sqlite3_prepare_v2(db, add_crossed_volume_sql, sizeof(add_crossed_volume_sql), &stmt, NULL);
+ if (ret != SQLITE_OK) {
+ xlog(L_WARNING, "Unable to prepare SQL query: %s", sqlite3_errstr(ret));
+ ret = -1;
+ goto out;
+ }
+
+ ret = sqlite3_bind_text(stmt, 1, path, -1, NULL);
+ if (ret != SQLITE_OK) {
+ xlog(L_WARNING, "Unable to bind \"%s\" SQL query: %s", __func__, sqlite3_errstr(ret));
+ ret = -1;
+ goto out;
+ }
+
+ ret = sqlite3_step(stmt);
+ if (ret != SQLITE_DONE) {
+ xlog(L_WARNING, "Error while adding \"%s\" from database: %s", path, sqlite3_errstr(ret));
+ ret = -1;
+ } else {
+ ret = 0;
+ }
+
+out:
+ reexpdb_unlock();
+ sqlite3_finalize(stmt);
+ return ret;
+}
+
+int reexpdb_drop_subvolume_unlocked(char *path)
+{
+ sqlite3_stmt *stmt = NULL;
+ int ret;
+
+ ret = sqlite3_prepare_v2(db, drop_crossed_volume_sql, sizeof(drop_crossed_volume_sql), &stmt, NULL);
+ if (ret != SQLITE_OK) {
+ xlog(L_WARNING, "Unable to prepare SQL query: %s", sqlite3_errstr(ret));
+ ret = -1;
+ goto out;
+ }
+
+ ret = sqlite3_bind_text(stmt, 1, path, -1, NULL);
+ if (ret != SQLITE_OK) {
+ xlog(L_WARNING, "Unable to bind \"%s\" SQL query: %s", __func__, sqlite3_errstr(ret));
+ ret = -1;
+ goto out;
+ }
+
+ ret = sqlite3_step(stmt);
+ if (ret != SQLITE_DONE) {
+ xlog(L_WARNING, "Error while deleting \"%s\" from database: %s", path, sqlite3_errstr(ret));
+ ret = -1;
+ } else {
+ ret = 0;
+ }
+
+out:
+ sqlite3_finalize(stmt);
+ return ret;
+}
+
+
+int reexpdb_uncover_subvolumes(void (*cb)(char *path))
+{
+ sqlite3_stmt *stmt = NULL;
+ struct statfs st;
+ const unsigned char *path;
+ int ret;
+
+ if (cb)
+ reexpdb_wrlock();
+ else
+ reexpdb_rdlock();
+
+ ret = sqlite3_prepare_v2(db, get_crossed_volumes_sql, sizeof(get_crossed_volumes_sql), &stmt, NULL);
+ if (ret != SQLITE_OK) {
+ xlog(L_WARNING, "Unable to prepare SQL query: %s", sqlite3_errstr(ret));
+ ret = -1;
+ goto out;
+ }
+
+ for (;;) {
+ ret = sqlite3_step(stmt);
+ if (ret != SQLITE_ROW)
+ break;
+
+ path = sqlite3_column_text(stmt, 0);
+ if (cb)
+ cb((char *)path);
+ else
+ statfs((char *)path, &st);
+ }
+
+ if (ret != SQLITE_DONE) {
+ xlog(L_WARNING, "Error while reading all subvolumes: %s", sqlite3_errstr(ret));
+ ret = -1;
+ goto out_unlock;
+ }
+
+ ret = 0;
+
+out_unlock:
+ reexpdb_unlock();
+ sqlite3_finalize(stmt);
+out:
+ return ret;
+}
diff --git a/support/reexport/reexport.h b/support/reexport/reexport.h
new file mode 100644
index 00000000..46ec8a96
--- /dev/null
+++ b/support/reexport/reexport.h
@@ -0,0 +1,53 @@
+#ifndef REEXPORT_H
+#define REEXPORT_H
+
+enum {
+ REEXP_NONE = 0,
+ REEXP_AUTO_FSIDNUM,
+ REEXP_PREDEFINED_FSIDNUM,
+ REEXP_REMOTE_DEVFSID,
+};
+
+#ifdef HAVE_REEXPORT_SUPPORT
+int reexpdb_init(void);
+void reexpdb_destroy(void);
+int reexpdb_fsidnum_by_path(char *path, uint32_t *fsidnum, int may_create);
+int reexpdb_apply_reexport_settings(struct exportent *ep, char *flname, int flline);
+int reexpdb_add_subvolume(char *path);
+int reexpdb_uncover_subvolumes(void (*cb)(char *path));
+int reexpdb_drop_subvolume_unlocked(char *path);
+#else
+static inline int reexpdb_init(void) { return 0; }
+static inline void reexpdb_destroy(void) {}
+static inline int reexpdb_fsidnum_by_path(char *path, uint32_t *fsidnum, int may_create)
+{
+ (void)path;
+ (void)may_create;
+ *fsidnum = 0;
+ return 0;
+}
+static inline int reexpdb_apply_reexport_settings(struct exportent *ep, char *flname, int flline)
+{
+ (void)ep;
+ (void)flname;
+ (void)flline;
+ return 0;
+}
+static inline int reexpdb_add_subvolume(char *path)
+{
+ (void)path;
+ return 0;
+}
+static inline int reexpdb_uncover_subvolumes(void (*cb)(char *path))
+{
+ (void)cb;
+ return 0;
+}
+static inline int reexpdb_drop_subvolume_unlocked(char *path)
+{
+ (void)path;
+ return 0;
+}
+#endif /* HAVE_REEXPORT_SUPPORT */
+
+#endif /* REEXPORT_H */
--
2.31.1
next prev parent reply other threads:[~2022-02-17 13:23 UTC|newest]
Thread overview: 24+ messages / expand[flat|nested] mbox.gz Atom feed top
2022-02-17 13:15 [RFC PATCH 0/6] nfs-utils: Improving NFS re-exports Richard Weinberger
2022-02-17 13:15 ` Richard Weinberger [this message]
2022-03-08 21:44 ` [RFC PATCH 1/6] Implement reexport helper library J. Bruce Fields
2022-03-09 9:43 ` Richard Weinberger
2022-03-09 14:19 ` bfields
2022-03-09 15:02 ` Richard Weinberger
2022-03-09 15:28 ` bfields
2022-02-17 13:15 ` [RFC PATCH 2/6] exports: Implement new export option reexport= Richard Weinberger
2022-03-08 22:10 ` J. Bruce Fields
2022-03-09 9:43 ` Richard Weinberger
2022-02-17 13:15 ` [RFC PATCH 3/6] export: Implement logic behind reexport= Richard Weinberger
2022-02-17 13:15 ` [RFC PATCH 4/6] export: Record mounted volumes Richard Weinberger
2022-02-17 13:15 ` [RFC PATCH 5/6] nfsd: statfs() every known subvolume upon start Richard Weinberger
2022-02-17 13:15 ` [RFC PATCH 6/6] export: Garbage collect orphaned subvolumes " Richard Weinberger
2022-02-17 16:33 ` [RFC PATCH 0/6] nfs-utils: Improving NFS re-exports J. Bruce Fields
2022-02-17 17:27 ` Richard Weinberger
2022-02-17 19:27 ` bfields
2022-02-17 20:15 ` Richard Weinberger
2022-02-17 20:18 ` bfields
2022-02-17 20:29 ` Richard Weinberger
2022-03-07 9:25 ` Richard Weinberger
2022-03-07 22:29 ` bfields
2022-04-19 20:20 ` Steve Dickson
2022-04-19 20:31 ` Richard Weinberger
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20220217131531.2890-2-richard@nod.at \
--to=richard@nod.at \
--cc=anna.schumaker@netapp.com \
--cc=bfields@fieldses.org \
--cc=chris.chilvers@appsbroker.com \
--cc=david.oberhollenzer@sigma-star.at \
--cc=david.young@appsbroker.com \
--cc=david@sigma-star.at \
--cc=linux-nfs@vger.kernel.org \
--cc=luis.turcitu@appsbroker.com \
--cc=trond.myklebust@hammerspace.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).