All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH pynfs 00/13] server41tests: add some tests for copy offload
@ 2026-07-09 19:02 Jeff Layton
  2026-07-09 19:02 ` [PATCH pynfs 01/13] server41tests: add helpers and basic synchronous COPY test Jeff Layton
                   ` (12 more replies)
  0 siblings, 13 replies; 14+ messages in thread
From: Jeff Layton @ 2026-07-09 19:02 UTC (permalink / raw)
  To: Calum Mackay
  Cc: Chuck Lever, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
	linux-nfs, Jeff Layton

This patchset adds a number of tests for copy offload. Most of them are
normal single-server testcases, but the last one adds a test of an
actual server-to-server copy from one server to another. This requires
a new command-line option for specifying the source server. When
omitted, the test is skipped.

This is based on top of my dir delegation pynfs tests, but I don't think
they touch much of the same code.

Signed-off-by: Jeff Layton <jlayton@kernel.org>
---
Jeff Layton (13):
      server41tests: add helpers and basic synchronous COPY test
      server41tests: test COPY with non-zero offsets
      server41tests: test async COPY with OFFLOAD_STATUS polling
      server41tests: test OFFLOAD_STATUS persists after async copy completes
      server41tests: test OFFLOAD_CANCEL on async copy
      server41tests: test COPY with bad source stateid
      server41tests: test COPY with bad destination stateid
      server41tests: test OFFLOAD_STATUS with fabricated stateid
      server41tests: test COPY within same file
      nfs4.1: fix COPY_NOTIFY args union arm name in XDR
      server41tests: test COPY_NOTIFY
      server41tests: support a second server for inter-server copy
      server41tests: test inter-server COPY

 CLAUDE.md                                          | 100 +++++
 ...ts-add-a-test-for-rename-within-a-dir-wit.patch |  55 +++
 nfs4.1/server41tests/environment.py                | 127 ++++--
 nfs4.1/server41tests/st_copy.py                    | 488 ++++++++++++++++++++-
 nfs4.1/testserver.py                               |  14 +
 nfs4.1/xdrdef/nfs4.x                               |   2 +-
 6 files changed, 740 insertions(+), 46 deletions(-)
---
base-commit: 95d7055e00ab861fb16f750d244dacc57a30ca19
change-id: 20260709-copy-df33bb31e88c

Best regards,
-- 
Jeff Layton <jlayton@kernel.org>


^ permalink raw reply	[flat|nested] 14+ messages in thread

* [PATCH pynfs 01/13] server41tests: add helpers and basic synchronous COPY test
  2026-07-09 19:02 [PATCH pynfs 00/13] server41tests: add some tests for copy offload Jeff Layton
@ 2026-07-09 19:02 ` Jeff Layton
  2026-07-09 19:02 ` [PATCH pynfs 02/13] server41tests: test COPY with non-zero offsets Jeff Layton
                   ` (11 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Jeff Layton @ 2026-07-09 19:02 UTC (permalink / raw)
  To: Calum Mackay
  Cc: Chuck Lever, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
	linux-nfs, Jeff Layton

Add helpers to reduce boilerplate in COPY tests: _do_copy(),
_create_and_open(), _poll_offload_status(), and chunked _write_data()/
_verify_data() that split I/O into pieces bounded by the session's
negotiated ca_maxrequestsize/ca_maxresponsesize (a single large WRITE or
READ would otherwise fail with NFS4ERR_REQ_TOO_BIG).

Add testSyncCopy (COPY1): write 64KB to a source file, copy it, and
verify the destination contents.  RFC 7862 permits a server to perform
the copy asynchronously even when a synchronous copy is requested, so
honor cr_resok4.cr_requirements.cr_synchronous: poll OFFLOAD_STATUS to
completion when the server downgrades to async instead of failing on a
zero wr_count.

Signed-off-by: Jeff Layton <jlayton@kernel.org>
---
 nfs4.1/server41tests/st_copy.py | 90 ++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 89 insertions(+), 1 deletion(-)

diff --git a/nfs4.1/server41tests/st_copy.py b/nfs4.1/server41tests/st_copy.py
index a4bdb77ca407..c7e48adf6fbe 100644
--- a/nfs4.1/server41tests/st_copy.py
+++ b/nfs4.1/server41tests/st_copy.py
@@ -1,14 +1,102 @@
+import time
+
 from .st_create_session import create_session
 from xdrdef.nfs4_const import *
 
 from .environment import check, fail, create_file, open_file, close_file
-from .environment import open_create_file_op, use_obj, write_file
+from .environment import open_create_file_op, use_obj, write_file, read_file
 from xdrdef.nfs4_type import open_owner4, openflag4, createhow4, open_claim4
 from xdrdef.nfs4_type import creatverfattr, fattr4, stateid4, locker4, lock_owner4
 from xdrdef.nfs4_type import open_to_lock_owner4
 import nfs_ops
 op = nfs_ops.NFS4ops()
 
+def _do_copy(sess, src_fh, src_stateid, dst_fh, dst_stateid,
+             src_offset=0, dst_offset=0, count=0,
+             consecutive=0, synchronous=1):
+    ops = [op.putfh(src_fh), op.savefh(), op.putfh(dst_fh),
+           op.copy(src_stateid, dst_stateid, src_offset, dst_offset,
+                   count, consecutive, synchronous, [])]
+    return sess.compound(ops)
+
+def _poll_offload_status(sess, dst_fh, copy_stateid, timeout=120):
+    deadline = time.time() + timeout
+    while time.time() < deadline:
+        ops = [op.putfh(dst_fh), op.offload_status(copy_stateid)]
+        res = sess.compound(ops)
+        check(res)
+        status_res = res.resarray[-1]
+        if status_res.osr_complete:
+            return status_res
+        time.sleep(1)
+    fail("OFFLOAD_STATUS did not complete within %d seconds" % timeout)
+
+def _create_and_open(sess, name):
+    res = create_file(sess, name)
+    check(res)
+    fh = res.resarray[-1].object
+    stateid = res.resarray[-2].stateid
+    return fh, stateid
+
+def _write_data(sess, fh, stateid, data, offset=0):
+    """Write data in chunks bounded by the session's max request size."""
+    chunk = sess.fore_channel.maxrequestsize - 1024
+    pos = 0
+    while pos < len(data):
+        res = write_file(sess, fh, data[pos:pos + chunk], offset + pos, stateid)
+        check(res, msg="WRITE at offset %d" % (offset + pos))
+        pos += res.count
+
+def _verify_data(sess, fh, stateid, data, offset=0):
+    """Read back and compare data in chunks bounded by max response size."""
+    chunk = sess.fore_channel.maxresponsesize - 1024
+    pos = 0
+    while pos < len(data):
+        res = read_file(sess, fh, offset + pos, min(chunk, len(data) - pos),
+                        stateid)
+        check(res)
+        if not res.data:
+            fail("Short read at offset %d" % (offset + pos))
+        if res.data != data[pos:pos + len(res.data)]:
+            fail("Data mismatch at offset %d" % (offset + pos))
+        pos += len(res.data)
+
+def testSyncCopy(t, env):
+    """synchronous copy of a file and verify contents
+
+    FLAGS: copy
+    CODE: COPY1
+    """
+    sess = env.c1.new_client_session(env.testname(t))
+    src_fh, src_stateid = _create_and_open(sess, env.testname(t))
+    data = b"A" * 65536
+    _write_data(sess, src_fh, src_stateid, data)
+
+    dst_fh, dst_stateid = _create_and_open(sess, env.testname(t) + b"_dst")
+
+    res = _do_copy(sess, src_fh, src_stateid, dst_fh, dst_stateid,
+                   count=len(data), synchronous=1)
+    check(res)
+    cr = res.resarray[-1]
+
+    # A synchronous COPY was requested, but RFC 7862 permits the server to
+    # perform the copy asynchronously anyway; cr_requirements.cr_synchronous
+    # reports what actually happened.  Honor either, but verify the byte count.
+    if cr.cr_resok4.cr_requirements.cr_synchronous:
+        if cr.cr_response.wr_count != len(data):
+            fail("Synchronous copy expected %d bytes, got %d" %
+                 (len(data), cr.cr_response.wr_count))
+    else:
+        copy_stateid = cr.cr_response.wr_callback_id[0]
+        status = _poll_offload_status(sess, dst_fh, copy_stateid)
+        if status.osr_complete[0] != NFS4_OK:
+            fail("Async copy completed with error: %d" % status.osr_complete[0])
+        if status.osr_count != len(data):
+            fail("Expected %d bytes copied, got %d" %
+                 (len(data), status.osr_count))
+
+    _verify_data(sess, dst_fh, dst_stateid, data)
+
 def testZeroLengthCopy(t, env):
     """test that zero-length copy copies to EOF
 

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH pynfs 02/13] server41tests: test COPY with non-zero offsets
  2026-07-09 19:02 [PATCH pynfs 00/13] server41tests: add some tests for copy offload Jeff Layton
  2026-07-09 19:02 ` [PATCH pynfs 01/13] server41tests: add helpers and basic synchronous COPY test Jeff Layton
@ 2026-07-09 19:02 ` Jeff Layton
  2026-07-09 19:02 ` [PATCH pynfs 03/13] server41tests: test async COPY with OFFLOAD_STATUS polling Jeff Layton
                   ` (10 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Jeff Layton @ 2026-07-09 19:02 UTC (permalink / raw)
  To: Calum Mackay
  Cc: Chuck Lever, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
	linux-nfs, Jeff Layton

Add testCopyWithOffset (COPY2) which copies 4KB from source offset
1024 to destination offset 512, then reads back the destination to
verify the data landed at the correct position.

Signed-off-by: Jeff Layton <jlayton@kernel.org>
---
 CLAUDE.md                                          | 100 +++++++++++++++++++++
 ...ts-add-a-test-for-rename-within-a-dir-wit.patch |  55 ++++++++++++
 nfs4.1/server41tests/st_copy.py                    |  25 ++++++
 3 files changed, 180 insertions(+)

diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 000000000000..91a499884990
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,100 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## What is pynfs?
+
+pynfs is an NFS protocol conformance testing framework written in Python. It tests NFSv4.0 and NFSv4.1/4.2 servers by sending crafted NFS COMPOUND operations over RPC and verifying responses. Tests require a live NFS server with an exported filesystem.
+
+## Build
+
+```bash
+# Install dependencies (Fedora)
+yum install krb5-devel python3-devel swig python3-gssapi python3-ply
+pip install xdrlib3
+
+# Build (generates XDR code from .x files -- must be done before running tests)
+./setup.py build
+```
+
+Build order matters: xdr → rpc → nfs4.1 → nfs4.0. The top-level setup.py handles this.
+
+## Running Tests
+
+Tests use a **custom test framework** (not pytest). They require a live NFS server.
+
+```bash
+cd nfs4.1
+
+# First run: create test directory tree on server
+./testserver.py SERVER:/export --maketree -v all
+
+# Run all tests
+./testserver.py SERVER:/export -v all
+
+# Run a single test by code
+./testserver.py SERVER:/export OPEN1
+
+# Run a flag group
+./testserver.py SERVER:/export dirdeleg
+
+# Exclude tests: prefix with "no"
+./testserver.py SERVER:/export all nodirdeleg
+./testserver.py SERVER:/export dirdeleg noDIRDELEG3
+
+# Run with dependencies auto-resolved
+./testserver.py SERVER:/export --rundeps DIRDELEG5
+
+# Skip initial tree cleanup (faster for tests that don't need it)
+./testserver.py SERVER:/export --noinit EXID1
+```
+
+NFSv4.0 tests work the same way from the `nfs4.0/` directory.
+
+## Architecture
+
+- **`xdr/`** — XDR parser/code generator (`xdrgen.py` uses PLY). Generates `*_const.py`, `*_type.py`, `*_pack.py` from `.x` definition files. These generated files are gitignored.
+- **`rpc/`** — Shared RPC client library with AUTH_SYS and RPCSEC_GSS support.
+- **`nfs4.1/`** — NFSv4.1/4.2 test suite and utilities. Active development happens here.
+- **`nfs4.0/`** — NFSv4.0 test suite. Older codebase with its own copy of rpc/testmod in `lib/`.
+- **`nfs4.1/testmod.py`** — The custom test framework engine (discovery, dependencies, execution).
+- **`nfs4.1/nfs4client.py`** — NFS4 client implementation built on RPC.
+- **`nfs4.1/nfs_ops.py`** — Operation factory (`op.open()`, `op.putfh()`, `op.rename()`, etc.).
+
+## Writing Tests (NFSv4.1)
+
+Tests live in `nfs4.1/server41tests/st_*.py`. Each test is a function:
+
+```python
+def testMyFeature(t, env):
+    """Describe what this test verifies
+
+    FLAGS: myfeature all
+    CODE: MYFEAT1
+    DEPEND: MYFEAT0
+    """
+    sess = env.c1.new_client_session(env.testname(t))
+    res = sess.compound([op.putrootfh()])
+    check(res)
+```
+
+Key conventions:
+- Function name starts with `test`, takes `(t, env)`.
+- **CODE** (required): unique identifier like `OPEN1`, `DIRDELEG5`.
+- **FLAGS**: space-separated group tags. Include `all` for general tests.
+- **DEPEND**: test codes that must pass first.
+- **VERS**: minor version range (e.g., `1-2`).
+- `check(res, NFS4_OK)` is the primary assertion — compares compound result status.
+- `t.fail("msg")` signals failure; `t.pass_warn("msg")` for warnings; `t.fail_support("msg")` for unsupported features.
+- New test modules must be added to `server41tests/__init__.py`'s `__all__` list.
+- Environment helpers in `server41tests/environment.py`: `create_file()`, `open_file()`, `close_file()`, `clean_dir()`, `do_readdir()`, etc.
+
+## Contributing
+
+Patches go to linux-nfs@vger.kernel.org via `git format-patch` / `git send-email`. Commits should be signed off (`git commit -s`). Follow Linux kernel patch submission conventions.
+
+## Notes
+
+- The NFS server under test must allow high-port connections (`insecure` export option on Linux).
+- `use_local.py` in each package manipulates `sys.path` so tests can run directly from the source tree without installation.
+- Test results are not authoritative protocol statements — consult the RFCs if a server fails a test.
diff --git a/nfs4.1/0001-server41tests-add-a-test-for-rename-within-a-dir-wit.patch b/nfs4.1/0001-server41tests-add-a-test-for-rename-within-a-dir-wit.patch
new file mode 100644
index 000000000000..7e855592da85
--- /dev/null
+++ b/nfs4.1/0001-server41tests-add-a-test-for-rename-within-a-dir-wit.patch
@@ -0,0 +1,55 @@
+From 0ce942390b36e00547ea9ea56afdc449a9a50699 Mon Sep 17 00:00:00 2001
+From: Jeff Layton <jlayton@kernel.org>
+Date: Wed, 28 May 2025 12:10:22 -0400
+Subject: [PATCH] server41tests: add a test for rename within a dir with a
+ delegation
+
+Signed-off-by: Jeff Layton <jlayton@kernel.org>
+---
+ nfs4.1/server41tests/st_dir_deleg.py | 33 ++++++++++++++++++++++++++++
+ 1 file changed, 33 insertions(+)
+
+diff --git a/nfs4.1/server41tests/st_dir_deleg.py b/nfs4.1/server41tests/st_dir_deleg.py
+index 1fbeb6e0efb9..505e46680197 100644
+--- a/nfs4.1/server41tests/st_dir_deleg.py
++++ b/nfs4.1/server41tests/st_dir_deleg.py
+@@ -102,3 +102,36 @@ def testDirDelegRemove(t, env):
+ 
+     if (not completed):
+         fail("I didn't receive a CB_NOTIFY from the server!")
++
++def testDirDelegRename(t, env):
++    """Create a dir_deleg that accepts notification of RENAME events
++
++    FLAGS: dirdeleg all
++    CODE: DIRDELEG2
++    """
++    c = env.c1
++    recall = threading.Event()
++    notify = threading.Event()
++    sess1, fh, deleg = _getDirDeleg(t, env, [NOTIFY4_RENAME_ENTRY], recall, notify)
++
++    claim = open_claim4(CLAIM_NULL, env.testname(t))
++    owner = open_owner4(0, b"owner")
++    how = openflag4(OPEN4_CREATE, createhow4(GUARDED4, {FATTR4_SIZE:0}))
++    open_op = [ op.putfh(fh), op.open(0, OPEN4_SHARE_ACCESS_WRITE,
++                                      OPEN4_SHARE_DENY_NONE, owner, how, claim) ]
++    res = sess1.compound(open_op)
++    check(res)
++
++    sess2 = c.new_client_session(b"%s_2" % env.testname(t))
++    topdir = c.homedir + [t.code.encode('utf8')]
++    oldpath = b"%s/%s" % (topdir, env.testname(t))
++    newpath = b"%s_2" % oldpath)
++    res = rename_obj(sess2, oldpath, newpath)
++    check(res)
++
++    completed = notify.wait(5)
++    ops = [ op.putfh(fh), op.delegreturn(deleg) ]
++    res = sess1.compound(ops)
++
++    if (not completed):
++        fail("I didn't receive a CB_NOTIFY from the server!")
+-- 
+2.49.0
+
diff --git a/nfs4.1/server41tests/st_copy.py b/nfs4.1/server41tests/st_copy.py
index c7e48adf6fbe..bfc64bbe1584 100644
--- a/nfs4.1/server41tests/st_copy.py
+++ b/nfs4.1/server41tests/st_copy.py
@@ -97,6 +97,31 @@ def testSyncCopy(t, env):
 
     _verify_data(sess, dst_fh, dst_stateid, data)
 
+def testCopyWithOffset(t, env):
+    """copy with non-zero source and destination offsets
+
+    FLAGS: copy
+    CODE: COPY2
+    """
+    sess = env.c1.new_client_session(env.testname(t))
+    src_fh, src_stateid = _create_and_open(sess, env.testname(t))
+    data = b"\x00" * 1024 + b"B" * 4096 + b"\x00" * 1024
+    _write_data(sess, src_fh, src_stateid, data)
+
+    dst_fh, dst_stateid = _create_and_open(sess, env.testname(t) + b"_dst")
+
+    res = _do_copy(sess, src_fh, src_stateid, dst_fh, dst_stateid,
+                   src_offset=1024, dst_offset=512, count=4096, synchronous=1)
+    check(res)
+    cr = res.resarray[-1]
+    if cr.cr_response.wr_count != 4096:
+        fail("Expected to copy 4096 bytes, got %d" % cr.cr_response.wr_count)
+
+    res = read_file(sess, dst_fh, 512, 4096, dst_stateid)
+    check(res)
+    if res.data != b"B" * 4096:
+        fail("Destination data at offset 512 does not match expected content")
+
 def testZeroLengthCopy(t, env):
     """test that zero-length copy copies to EOF
 

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH pynfs 03/13] server41tests: test async COPY with OFFLOAD_STATUS polling
  2026-07-09 19:02 [PATCH pynfs 00/13] server41tests: add some tests for copy offload Jeff Layton
  2026-07-09 19:02 ` [PATCH pynfs 01/13] server41tests: add helpers and basic synchronous COPY test Jeff Layton
  2026-07-09 19:02 ` [PATCH pynfs 02/13] server41tests: test COPY with non-zero offsets Jeff Layton
@ 2026-07-09 19:02 ` Jeff Layton
  2026-07-09 19:02 ` [PATCH pynfs 04/13] server41tests: test OFFLOAD_STATUS persists after async copy completes Jeff Layton
                   ` (9 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Jeff Layton @ 2026-07-09 19:02 UTC (permalink / raw)
  To: Calum Mackay
  Cc: Chuck Lever, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
	linux-nfs, Jeff Layton

Add _poll_offload_status() helper that polls OFFLOAD_STATUS in a loop
until the copy completes or a timeout is reached.

Add testAsyncCopy (COPY3) which writes 1MB to a source file, requests
an asynchronous copy (ca_synchronous=0), polls OFFLOAD_STATUS until
completion, and verifies the destination file contents.  If the server
chooses to perform the copy synchronously, the test still verifies the
result.

Signed-off-by: Jeff Layton <jlayton@kernel.org>
---
 nfs4.1/server41tests/st_copy.py | 33 +++++++++++++++++++++++++++++++++
 1 file changed, 33 insertions(+)

diff --git a/nfs4.1/server41tests/st_copy.py b/nfs4.1/server41tests/st_copy.py
index bfc64bbe1584..8c320173db1f 100644
--- a/nfs4.1/server41tests/st_copy.py
+++ b/nfs4.1/server41tests/st_copy.py
@@ -122,6 +122,39 @@ def testCopyWithOffset(t, env):
     if res.data != b"B" * 4096:
         fail("Destination data at offset 512 does not match expected content")
 
+def testAsyncCopy(t, env):
+    """request async copy, poll OFFLOAD_STATUS, verify data
+
+    FLAGS: copy
+    CODE: COPY3
+    """
+    sess = env.c1.new_client_session(env.testname(t))
+    src_fh, src_stateid = _create_and_open(sess, env.testname(t))
+    data = b"C" * (1024 * 1024)
+    _write_data(sess, src_fh, src_stateid, data)
+
+    dst_fh, dst_stateid = _create_and_open(sess, env.testname(t) + b"_dst")
+
+    res = _do_copy(sess, src_fh, src_stateid, dst_fh, dst_stateid,
+                   count=len(data), synchronous=0)
+    check(res)
+    cr = res.resarray[-1]
+
+    if not cr.cr_resok4.cr_requirements.cr_synchronous:
+        copy_stateid = cr.cr_response.wr_callback_id[0]
+        status = _poll_offload_status(sess, dst_fh, copy_stateid)
+        if status.osr_complete[0] != NFS4_OK:
+            fail("Async copy completed with error: %d" % status.osr_complete[0])
+        if status.osr_count != len(data):
+            fail("Expected %d bytes copied, got %d" %
+                 (len(data), status.osr_count))
+    else:
+        if cr.cr_response.wr_count != len(data):
+            fail("Sync copy returned %d bytes, expected %d" %
+                 (cr.cr_response.wr_count, len(data)))
+
+    _verify_data(sess, dst_fh, dst_stateid, data)
+
 def testZeroLengthCopy(t, env):
     """test that zero-length copy copies to EOF
 

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH pynfs 04/13] server41tests: test OFFLOAD_STATUS persists after async copy completes
  2026-07-09 19:02 [PATCH pynfs 00/13] server41tests: add some tests for copy offload Jeff Layton
                   ` (2 preceding siblings ...)
  2026-07-09 19:02 ` [PATCH pynfs 03/13] server41tests: test async COPY with OFFLOAD_STATUS polling Jeff Layton
@ 2026-07-09 19:02 ` Jeff Layton
  2026-07-09 19:02 ` [PATCH pynfs 05/13] server41tests: test OFFLOAD_CANCEL on async copy Jeff Layton
                   ` (8 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Jeff Layton @ 2026-07-09 19:02 UTC (permalink / raw)
  To: Calum Mackay
  Cc: Chuck Lever, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
	linux-nfs, Jeff Layton

Add testAsyncCopyOffloadStatusAfterComplete (COPY6) which performs an
async copy, waits for completion, then re-queries OFFLOAD_STATUS after
a delay to verify the server still returns valid copy state.

This catches a kernel bug where the async copy reaper has an inverted
TTL check (if (--cp_ttl) instead of if (!--cp_ttl)), causing the copy
state to be destroyed on the first laundromat tick after completion
rather than surviving for the full TTL window.

Signed-off-by: Jeff Layton <jlayton@kernel.org>
---
 nfs4.1/server41tests/st_copy.py | 47 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 47 insertions(+)

diff --git a/nfs4.1/server41tests/st_copy.py b/nfs4.1/server41tests/st_copy.py
index 8c320173db1f..4b207d35049d 100644
--- a/nfs4.1/server41tests/st_copy.py
+++ b/nfs4.1/server41tests/st_copy.py
@@ -178,3 +178,50 @@ def testZeroLengthCopy(t, env):
     l = res.resarray[-1].cr_response.wr_count
     if l != len(data):
         fail("Copy to end of %d-byte file copied %d bytes" % (len(data), l))
+
+def testAsyncCopyOffloadStatusAfterComplete(t, env):
+    """verify OFFLOAD_STATUS works after async copy completes
+
+    The server should keep copy state around for a TTL window after
+    completion so clients can query final status.  This catches the
+    inverted TTL check bug where the reaper destroys the state on
+    the first tick instead of the last.
+
+    FLAGS: copy
+    CODE: COPY6
+    """
+    sess = env.c1.new_client_session(env.testname(t))
+    src_fh, src_stateid = _create_and_open(sess, env.testname(t))
+    data = b"D" * (1024 * 1024)
+    _write_data(sess, src_fh, src_stateid, data)
+
+    dst_fh, dst_stateid = _create_and_open(sess, env.testname(t) + b"_dst")
+
+    res = _do_copy(sess, src_fh, src_stateid, dst_fh, dst_stateid,
+                   count=len(data), synchronous=0)
+    check(res)
+    cr = res.resarray[-1]
+
+    if cr.cr_resok4.cr_requirements.cr_synchronous:
+        if cr.cr_response.wr_count != len(data):
+            fail("Sync copy returned %d bytes, expected %d" %
+                 (cr.cr_response.wr_count, len(data)))
+        return
+
+    copy_stateid = cr.cr_response.wr_callback_id[0]
+    status = _poll_offload_status(sess, dst_fh, copy_stateid)
+    if status.osr_complete[0] != NFS4_OK:
+        fail("Async copy completed with error: %d" % status.osr_complete[0])
+
+    # Copy is done. Wait a bit then re-query -- state should still be valid.
+    time.sleep(5)
+
+    ops = [op.putfh(dst_fh), op.offload_status(copy_stateid)]
+    res = sess.compound(ops)
+    check(res, msg="OFFLOAD_STATUS after completion should still succeed")
+    recheck = res.resarray[-1]
+    if not recheck.osr_complete:
+        fail("OFFLOAD_STATUS lost completion status")
+    if recheck.osr_complete[0] != NFS4_OK:
+        fail("OFFLOAD_STATUS completion changed to error: %d" %
+             recheck.osr_complete[0])

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH pynfs 05/13] server41tests: test OFFLOAD_CANCEL on async copy
  2026-07-09 19:02 [PATCH pynfs 00/13] server41tests: add some tests for copy offload Jeff Layton
                   ` (3 preceding siblings ...)
  2026-07-09 19:02 ` [PATCH pynfs 04/13] server41tests: test OFFLOAD_STATUS persists after async copy completes Jeff Layton
@ 2026-07-09 19:02 ` Jeff Layton
  2026-07-09 19:02 ` [PATCH pynfs 06/13] server41tests: test COPY with bad source stateid Jeff Layton
                   ` (7 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Jeff Layton @ 2026-07-09 19:02 UTC (permalink / raw)
  To: Calum Mackay
  Cc: Chuck Lever, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
	linux-nfs, Jeff Layton

Add testOffloadCancel (COPY7) which starts an async copy and
immediately issues OFFLOAD_CANCEL.  Accepts NFS4_OK, NFS4ERR_NOTSUPP,
or NFS4ERR_COMPLETE_ALREADY as valid responses since the copy may
finish before the cancel arrives.

Signed-off-by: Jeff Layton <jlayton@kernel.org>
---
 nfs4.1/server41tests/st_copy.py | 28 ++++++++++++++++++++++++++++
 1 file changed, 28 insertions(+)

diff --git a/nfs4.1/server41tests/st_copy.py b/nfs4.1/server41tests/st_copy.py
index 4b207d35049d..897c8c7d3a6c 100644
--- a/nfs4.1/server41tests/st_copy.py
+++ b/nfs4.1/server41tests/st_copy.py
@@ -225,3 +225,31 @@ def testAsyncCopyOffloadStatusAfterComplete(t, env):
     if recheck.osr_complete[0] != NFS4_OK:
         fail("OFFLOAD_STATUS completion changed to error: %d" %
              recheck.osr_complete[0])
+
+def testOffloadCancel(t, env):
+    """start an async copy and cancel it with OFFLOAD_CANCEL
+
+    FLAGS: copy
+    CODE: COPY7
+    """
+    sess = env.c1.new_client_session(env.testname(t))
+    src_fh, src_stateid = _create_and_open(sess, env.testname(t))
+    data = b"E" * (1024 * 1024)
+    _write_data(sess, src_fh, src_stateid, data)
+
+    dst_fh, dst_stateid = _create_and_open(sess, env.testname(t) + b"_dst")
+
+    res = _do_copy(sess, src_fh, src_stateid, dst_fh, dst_stateid,
+                   count=len(data), synchronous=0)
+    check(res)
+    cr = res.resarray[-1]
+
+    if cr.cr_resok4.cr_requirements.cr_synchronous:
+        return
+
+    copy_stateid = cr.cr_response.wr_callback_id[0]
+
+    ops = [op.putfh(dst_fh), op.offload_cancel(copy_stateid)]
+    res = sess.compound(ops)
+    check(res, [NFS4_OK, NFS4ERR_NOTSUPP, NFS4ERR_COMPLETE_ALREADY],
+          msg="OFFLOAD_CANCEL")

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH pynfs 06/13] server41tests: test COPY with bad source stateid
  2026-07-09 19:02 [PATCH pynfs 00/13] server41tests: add some tests for copy offload Jeff Layton
                   ` (4 preceding siblings ...)
  2026-07-09 19:02 ` [PATCH pynfs 05/13] server41tests: test OFFLOAD_CANCEL on async copy Jeff Layton
@ 2026-07-09 19:02 ` Jeff Layton
  2026-07-09 19:02 ` [PATCH pynfs 07/13] server41tests: test COPY with bad destination stateid Jeff Layton
                   ` (6 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Jeff Layton @ 2026-07-09 19:02 UTC (permalink / raw)
  To: Calum Mackay
  Cc: Chuck Lever, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
	linux-nfs, Jeff Layton

Add testCopyBadSourceStateid (COPY8) and a _bad_stateid() helper that
returns a fabricated, non-special stateid -- not the all-zero anonymous
or all-one READ-bypass special stateids, which a server legitimately
accepts -- so the server is forced to reject it with NFS4ERR_BAD_STATEID.

Signed-off-by: Jeff Layton <jlayton@kernel.org>
---
 nfs4.1/server41tests/st_copy.py | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)

diff --git a/nfs4.1/server41tests/st_copy.py b/nfs4.1/server41tests/st_copy.py
index 897c8c7d3a6c..1918f3da2bdf 100644
--- a/nfs4.1/server41tests/st_copy.py
+++ b/nfs4.1/server41tests/st_copy.py
@@ -38,6 +38,11 @@ def _create_and_open(sess, name):
     stateid = res.resarray[-2].stateid
     return fh, stateid
 
+def _bad_stateid():
+    # A fabricated, non-special stateid (not the all-zero anonymous or
+    # all-one READ-bypass special stateids) the server cannot recognize.
+    return stateid4(1, b'\xde\xad\xbe\xef' * 3)
+
 def _write_data(sess, fh, stateid, data, offset=0):
     """Write data in chunks bounded by the session's max request size."""
     chunk = sess.fore_channel.maxrequestsize - 1024
@@ -253,3 +258,17 @@ def testOffloadCancel(t, env):
     res = sess.compound(ops)
     check(res, [NFS4_OK, NFS4ERR_NOTSUPP, NFS4ERR_COMPLETE_ALREADY],
           msg="OFFLOAD_CANCEL")
+
+def testCopyBadSourceStateid(t, env):
+    """COPY with an invalid source stateid should fail
+
+    FLAGS: copy
+    CODE: COPY8
+    """
+    sess = env.c1.new_client_session(env.testname(t))
+    src_fh, _src_stateid = _create_and_open(sess, env.testname(t))
+    dst_fh, dst_stateid = _create_and_open(sess, env.testname(t) + b"_dst")
+
+    res = _do_copy(sess, src_fh, _bad_stateid(), dst_fh, dst_stateid,
+                   count=1024, synchronous=1)
+    check(res, NFS4ERR_BAD_STATEID, msg="COPY with bad source stateid")

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH pynfs 07/13] server41tests: test COPY with bad destination stateid
  2026-07-09 19:02 [PATCH pynfs 00/13] server41tests: add some tests for copy offload Jeff Layton
                   ` (5 preceding siblings ...)
  2026-07-09 19:02 ` [PATCH pynfs 06/13] server41tests: test COPY with bad source stateid Jeff Layton
@ 2026-07-09 19:02 ` Jeff Layton
  2026-07-09 19:02 ` [PATCH pynfs 08/13] server41tests: test OFFLOAD_STATUS with fabricated stateid Jeff Layton
                   ` (5 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Jeff Layton @ 2026-07-09 19:02 UTC (permalink / raw)
  To: Calum Mackay
  Cc: Chuck Lever, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
	linux-nfs, Jeff Layton

Add testCopyBadDestStateid (COPY9) which verifies the server returns
NFS4ERR_BAD_STATEID when the COPY destination stateid is invalid.

Signed-off-by: Jeff Layton <jlayton@kernel.org>
---
 nfs4.1/server41tests/st_copy.py | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/nfs4.1/server41tests/st_copy.py b/nfs4.1/server41tests/st_copy.py
index 1918f3da2bdf..6be3c70ccfab 100644
--- a/nfs4.1/server41tests/st_copy.py
+++ b/nfs4.1/server41tests/st_copy.py
@@ -272,3 +272,18 @@ def testCopyBadSourceStateid(t, env):
     res = _do_copy(sess, src_fh, _bad_stateid(), dst_fh, dst_stateid,
                    count=1024, synchronous=1)
     check(res, NFS4ERR_BAD_STATEID, msg="COPY with bad source stateid")
+
+def testCopyBadDestStateid(t, env):
+    """COPY with an invalid destination stateid should fail
+
+    FLAGS: copy
+    CODE: COPY9
+    """
+    sess = env.c1.new_client_session(env.testname(t))
+    src_fh, src_stateid = _create_and_open(sess, env.testname(t))
+    write_file(sess, src_fh, b"data", 0, src_stateid)
+    dst_fh, _dst_stateid = _create_and_open(sess, env.testname(t) + b"_dst")
+
+    res = _do_copy(sess, src_fh, src_stateid, dst_fh, _bad_stateid(),
+                   count=4, synchronous=1)
+    check(res, NFS4ERR_BAD_STATEID, msg="COPY with bad dest stateid")

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH pynfs 08/13] server41tests: test OFFLOAD_STATUS with fabricated stateid
  2026-07-09 19:02 [PATCH pynfs 00/13] server41tests: add some tests for copy offload Jeff Layton
                   ` (6 preceding siblings ...)
  2026-07-09 19:02 ` [PATCH pynfs 07/13] server41tests: test COPY with bad destination stateid Jeff Layton
@ 2026-07-09 19:02 ` Jeff Layton
  2026-07-09 19:02 ` [PATCH pynfs 09/13] server41tests: test COPY within same file Jeff Layton
                   ` (4 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Jeff Layton @ 2026-07-09 19:02 UTC (permalink / raw)
  To: Calum Mackay
  Cc: Chuck Lever, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
	linux-nfs, Jeff Layton

Add testOffloadStatusNoState (COPY10) which sends OFFLOAD_STATUS with
a fabricated stateid and verifies the server returns
NFS4ERR_BAD_STATEID.

Signed-off-by: Jeff Layton <jlayton@kernel.org>
---
 nfs4.1/server41tests/st_copy.py | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/nfs4.1/server41tests/st_copy.py b/nfs4.1/server41tests/st_copy.py
index 6be3c70ccfab..bec92e8d86b2 100644
--- a/nfs4.1/server41tests/st_copy.py
+++ b/nfs4.1/server41tests/st_copy.py
@@ -287,3 +287,16 @@ def testCopyBadDestStateid(t, env):
     res = _do_copy(sess, src_fh, src_stateid, dst_fh, _bad_stateid(),
                    count=4, synchronous=1)
     check(res, NFS4ERR_BAD_STATEID, msg="COPY with bad dest stateid")
+
+def testOffloadStatusNoState(t, env):
+    """OFFLOAD_STATUS with a fabricated stateid should fail
+
+    FLAGS: copy
+    CODE: COPY10
+    """
+    sess = env.c1.new_client_session(env.testname(t))
+    src_fh, _stateid = _create_and_open(sess, env.testname(t))
+
+    ops = [op.putfh(src_fh), op.offload_status(_bad_stateid())]
+    res = sess.compound(ops)
+    check(res, NFS4ERR_BAD_STATEID, msg="OFFLOAD_STATUS with bad stateid")

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH pynfs 09/13] server41tests: test COPY within same file
  2026-07-09 19:02 [PATCH pynfs 00/13] server41tests: add some tests for copy offload Jeff Layton
                   ` (7 preceding siblings ...)
  2026-07-09 19:02 ` [PATCH pynfs 08/13] server41tests: test OFFLOAD_STATUS with fabricated stateid Jeff Layton
@ 2026-07-09 19:02 ` Jeff Layton
  2026-07-09 19:02 ` [PATCH pynfs 10/13] nfs4.1: fix COPY_NOTIFY args union arm name in XDR Jeff Layton
                   ` (3 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Jeff Layton @ 2026-07-09 19:02 UTC (permalink / raw)
  To: Calum Mackay
  Cc: Chuck Lever, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
	linux-nfs, Jeff Layton

Add testCopyToSameFile (COPY11) which writes 4KB at offset 0, copies
it to offset 8192 within the same file, and verifies the data at the
destination offset matches the source.

Signed-off-by: Jeff Layton <jlayton@kernel.org>
---
 nfs4.1/server41tests/st_copy.py | 23 +++++++++++++++++++++++
 1 file changed, 23 insertions(+)

diff --git a/nfs4.1/server41tests/st_copy.py b/nfs4.1/server41tests/st_copy.py
index bec92e8d86b2..1a392f9377b8 100644
--- a/nfs4.1/server41tests/st_copy.py
+++ b/nfs4.1/server41tests/st_copy.py
@@ -300,3 +300,26 @@ def testOffloadStatusNoState(t, env):
     ops = [op.putfh(src_fh), op.offload_status(_bad_stateid())]
     res = sess.compound(ops)
     check(res, NFS4ERR_BAD_STATEID, msg="OFFLOAD_STATUS with bad stateid")
+
+def testCopyToSameFile(t, env):
+    """copy within the same file to non-overlapping region
+
+    FLAGS: copy
+    CODE: COPY11
+    """
+    sess = env.c1.new_client_session(env.testname(t))
+    fh, stateid = _create_and_open(sess, env.testname(t))
+    data = b"F" * 4096
+    write_file(sess, fh, data, 0, stateid)
+
+    res = _do_copy(sess, fh, stateid, fh, stateid,
+                   src_offset=0, dst_offset=8192, count=4096, synchronous=1)
+    check(res)
+    cr = res.resarray[-1]
+    if cr.cr_response.wr_count != 4096:
+        fail("Expected to copy 4096 bytes, got %d" % cr.cr_response.wr_count)
+
+    res = read_file(sess, fh, 8192, 4096, stateid)
+    check(res)
+    if res.data != data:
+        fail("Same-file copy: data at offset 8192 does not match source")

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH pynfs 10/13] nfs4.1: fix COPY_NOTIFY args union arm name in XDR
  2026-07-09 19:02 [PATCH pynfs 00/13] server41tests: add some tests for copy offload Jeff Layton
                   ` (8 preceding siblings ...)
  2026-07-09 19:02 ` [PATCH pynfs 09/13] server41tests: test COPY within same file Jeff Layton
@ 2026-07-09 19:02 ` Jeff Layton
  2026-07-09 19:02 ` [PATCH pynfs 11/13] server41tests: test COPY_NOTIFY Jeff Layton
                   ` (2 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Jeff Layton @ 2026-07-09 19:02 UTC (permalink / raw)
  To: Calum Mackay
  Cc: Chuck Lever, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
	linux-nfs, Jeff Layton

The nfs_argop4 union arm for OP_COPY_NOTIFY was named opoffload_notify,
but nfs_ops derives the argop keyword from the operation name and so
looks for opcopy_notify (which is also the name of the COPY_NOTIFY4res
arm).  Because of the mismatch, op.copy_notify() raised TypeError and
the operation could not be constructed at all.

Rename the arm to opcopy_notify to match, enabling op.copy_notify().

Signed-off-by: Jeff Layton <jlayton@kernel.org>
---
 nfs4.1/xdrdef/nfs4.x | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/nfs4.1/xdrdef/nfs4.x b/nfs4.1/xdrdef/nfs4.x
index f03eb538a298..e836aa684f9b 100644
--- a/nfs4.1/xdrdef/nfs4.x
+++ b/nfs4.1/xdrdef/nfs4.x
@@ -3346,7 +3346,7 @@ union nfs_argop4 switch (nfs_opnum4 argop) {
  /* Operations new to NFSv4.2 */
  case OP_ALLOCATE:      ALLOCATE4args opallocate;
  case OP_COPY:          COPY4args opcopy;
- case OP_COPY_NOTIFY:   COPY_NOTIFY4args opoffload_notify;
+ case OP_COPY_NOTIFY:   COPY_NOTIFY4args opcopy_notify;
  case OP_DEALLOCATE:    DEALLOCATE4args opdeallocate;
  case OP_IO_ADVISE:     IO_ADVISE4args opio_advise;
  case OP_LAYOUTERROR: LAYOUTERROR4args  oplayouterror;

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH pynfs 11/13] server41tests: test COPY_NOTIFY
  2026-07-09 19:02 [PATCH pynfs 00/13] server41tests: add some tests for copy offload Jeff Layton
                   ` (9 preceding siblings ...)
  2026-07-09 19:02 ` [PATCH pynfs 10/13] nfs4.1: fix COPY_NOTIFY args union arm name in XDR Jeff Layton
@ 2026-07-09 19:02 ` Jeff Layton
  2026-07-09 19:02 ` [PATCH pynfs 12/13] server41tests: support a second server for inter-server copy Jeff Layton
  2026-07-09 19:02 ` [PATCH pynfs 13/13] server41tests: test inter-server COPY Jeff Layton
  12 siblings, 0 replies; 14+ messages in thread
From: Jeff Layton @ 2026-07-09 19:02 UTC (permalink / raw)
  To: Calum Mackay
  Cc: Chuck Lever, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
	linux-nfs, Jeff Layton

Add tests exercising the source-server half of NFSv4.2 inter-server copy
against a single server.  A client requesting an inter-server copy first
sends COPY_NOTIFY to the source server to authorize the destination and
obtain a source stateid and netloc list.

CPNOTIFY1 issues a basic COPY_NOTIFY and checks that a stateid and a
non-empty source_server list are returned; CPNOTIFY2 uses a bad source
stateid (expecting NFS4ERR_BAD_STATEID); CPNOTIFY3 omits the current
filehandle (expecting NFS4ERR_NOFILEHANDLE).  All three treat
NFS4ERR_NOTSUPP as unsupported so they are portable.

Signed-off-by: Jeff Layton <jlayton@kernel.org>
---
 nfs4.1/server41tests/st_copy.py | 115 +++++++++++++++++++++++++++++++++++++++-
 1 file changed, 114 insertions(+), 1 deletion(-)

diff --git a/nfs4.1/server41tests/st_copy.py b/nfs4.1/server41tests/st_copy.py
index 1a392f9377b8..7eb6e2c98c4d 100644
--- a/nfs4.1/server41tests/st_copy.py
+++ b/nfs4.1/server41tests/st_copy.py
@@ -1,3 +1,4 @@
+import socket
 import time
 
 from .st_create_session import create_session
@@ -7,7 +8,7 @@ from .environment import check, fail, create_file, open_file, close_file
 from .environment import open_create_file_op, use_obj, write_file, read_file
 from xdrdef.nfs4_type import open_owner4, openflag4, createhow4, open_claim4
 from xdrdef.nfs4_type import creatverfattr, fattr4, stateid4, locker4, lock_owner4
-from xdrdef.nfs4_type import open_to_lock_owner4
+from xdrdef.nfs4_type import open_to_lock_owner4, netloc4, netaddr4
 import nfs_ops
 op = nfs_ops.NFS4ops()
 
@@ -43,6 +44,23 @@ def _bad_stateid():
     # all-one READ-bypass special stateids) the server cannot recognize.
     return stateid4(1, b'\xde\xad\xbe\xef' * 3)
 
+def _server_netloc(env):
+    """netloc4 (NL4_NETADDR) naming the server under test.
+
+    The Linux server only accepts the NL4_NETADDR form of netloc4 and
+    rejects NL4_NAME / NL4_URL at decode time with NFS4ERR_BADXDR, so build
+    a universal address (RFC 1833: h.h.h.h.p1.p2) from the server address
+    and port.  For single-server COPY_NOTIFY tests this names the server
+    itself; we only need a netloc4 the source server will accept and record.
+    """
+    host, port = env.opts.server, env.opts.port
+    family, _, _, _, sockaddr = socket.getaddrinfo(
+        host, port, type=socket.SOCK_STREAM)[0]
+    ip = sockaddr[0]
+    uaddr = "%s.%d.%d" % (ip, (port >> 8) & 0xff, port & 0xff)
+    netid = b'tcp6' if family == socket.AF_INET6 else b'tcp'
+    return netloc4(NL4_NETADDR, nl_addr=netaddr4(netid, uaddr.encode('ascii')))
+
 def _write_data(sess, fh, stateid, data, offset=0):
     """Write data in chunks bounded by the session's max request size."""
     chunk = sess.fore_channel.maxrequestsize - 1024
@@ -323,3 +341,98 @@ def testCopyToSameFile(t, env):
     check(res)
     if res.data != data:
         fail("Same-file copy: data at offset 8192 does not match source")
+
+def testCopyNotify(t, env):
+    """COPY_NOTIFY authorizes a destination server to copy from the source
+
+    A client wanting an inter-server copy first sends COPY_NOTIFY to the
+    source server (CURRENT_FH = source file) naming the destination server.
+    The source returns a stateid and a list of netlocs the destination
+    should use to reach it; the client then hands these to the destination
+    server's COPY.  This exercises the source-server half against a single
+    server.
+
+    FLAGS: copy
+    CODE: CPNOTIFY1
+    VERS: 2-
+    """
+    sess = env.c1.new_client_session(env.testname(t))
+    src_fh, src_stateid = _create_and_open(sess, env.testname(t))
+    _write_data(sess, src_fh, src_stateid, b"copy notify test data")
+
+    ops = [op.putfh(src_fh),
+           op.copy_notify(src_stateid, _server_netloc(env))]
+    res = sess.compound(ops)
+    check(res, [NFS4_OK, NFS4ERR_NOTSUPP], msg="COPY_NOTIFY")
+    if res.status == NFS4ERR_NOTSUPP:
+        t.fail_support("Server does not support COPY_NOTIFY "
+                       "(inter-server copy source)")
+
+    cnr = res.resarray[-1]
+    # The client needs a source stateid to give to the destination's COPY.
+    if cnr.cnr_stateid is None:
+        fail("COPY_NOTIFY did not return a source stateid")
+    # And at least one netloc telling the destination how to reach the source.
+    if not cnr.cnr_source_server:
+        fail("COPY_NOTIFY returned an empty cnr_source_server list")
+
+def testCopyNotifyBadStateid(t, env):
+    """COPY_NOTIFY with an invalid source stateid should fail
+
+    FLAGS: copy
+    CODE: CPNOTIFY2
+    VERS: 2-
+    """
+    sess = env.c1.new_client_session(env.testname(t))
+    src_fh, _src_stateid = _create_and_open(sess, env.testname(t))
+
+    ops = [op.putfh(src_fh),
+           op.copy_notify(_bad_stateid(), _server_netloc(env))]
+    res = sess.compound(ops)
+    if res.status == NFS4ERR_NOTSUPP:
+        t.fail_support("Server does not support COPY_NOTIFY "
+                       "(inter-server copy source)")
+    check(res, NFS4ERR_BAD_STATEID, msg="COPY_NOTIFY with bad source stateid")
+
+def testCopyNotifyUnsupportedNetloc(t, env):
+    """COPY_NOTIFY with a name or URL netloc must not return NFS4ERR_BADXDR
+
+    NL4_NAME and NL4_URL are well-formed XDR, so a server that does not
+    support them must reject the operation with NFS4ERR_NOTSUPP, not
+    NFS4ERR_BADXDR (which is reserved for XDR that cannot be decoded).  A
+    server that does support them may return NFS4_OK.
+
+    FLAGS: copy
+    CODE: CPNOTIFY4
+    VERS: 2-
+    """
+    sess = env.c1.new_client_session(env.testname(t))
+    src_fh, src_stateid = _create_and_open(sess, env.testname(t))
+
+    for name, nl in [("NL4_NAME", netloc4(NL4_NAME, nl_name=b"nfs.example.org")),
+                     ("NL4_URL", netloc4(NL4_URL,
+                                         nl_url=b"nfs://nfs.example.org/"))]:
+        res = sess.compound([op.putfh(src_fh),
+                             op.copy_notify(src_stateid, nl)])
+        if res.status == NFS4ERR_BADXDR:
+            fail("COPY_NOTIFY with a %s netloc returned NFS4ERR_BADXDR; a "
+                 "well-formed but unsupported netloc should return "
+                 "NFS4ERR_NOTSUPP" % name)
+        check(res, [NFS4_OK, NFS4ERR_NOTSUPP],
+              msg="COPY_NOTIFY with a %s netloc" % name)
+
+def testCopyNotifyNoFh(t, env):
+    """COPY_NOTIFY without a current filehandle should fail
+
+    FLAGS: copy
+    CODE: CPNOTIFY3
+    VERS: 2-
+    """
+    sess = env.c1.new_client_session(env.testname(t))
+
+    ops = [op.copy_notify(env.stateid0, _server_netloc(env))]
+    res = sess.compound(ops)
+    if res.status == NFS4ERR_NOTSUPP:
+        t.fail_support("Server does not support COPY_NOTIFY "
+                       "(inter-server copy source)")
+    check(res, NFS4ERR_NOFILEHANDLE, msg="COPY_NOTIFY with no filehandle")

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH pynfs 12/13] server41tests: support a second server for inter-server copy
  2026-07-09 19:02 [PATCH pynfs 00/13] server41tests: add some tests for copy offload Jeff Layton
                   ` (10 preceding siblings ...)
  2026-07-09 19:02 ` [PATCH pynfs 11/13] server41tests: test COPY_NOTIFY Jeff Layton
@ 2026-07-09 19:02 ` Jeff Layton
  2026-07-09 19:02 ` [PATCH pynfs 13/13] server41tests: test inter-server COPY Jeff Layton
  12 siblings, 0 replies; 14+ messages in thread
From: Jeff Layton @ 2026-07-09 19:02 UTC (permalink / raw)
  To: Calum Mackay
  Cc: Chuck Lever, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
	linux-nfs, Jeff Layton

Inter-server (server-to-server) COPY tests need to talk to two servers.
Add a --server2 SERVER:/PATH option, parsed with parse_nfs_url, and an
optional second client env.c2 in the test environment.

The environment now creates, initializes, and cleans up the test tree on
both servers, and factors credential creation into a helper so c2 gets a
credential built the same way as c1.  When --server2 is not given, c2 is
None and single-server runs are unaffected.

Signed-off-by: Jeff Layton <jlayton@kernel.org>
---
 nfs4.1/server41tests/environment.py | 127 ++++++++++++++++++++++++------------
 nfs4.1/testserver.py                |  14 ++++
 2 files changed, 98 insertions(+), 43 deletions(-)

diff --git a/nfs4.1/server41tests/environment.py b/nfs4.1/server41tests/environment.py
index c14b12cbfca5..38cd5a7a7185 100644
--- a/nfs4.1/server41tests/environment.py
+++ b/nfs4.1/server41tests/environment.py
@@ -119,23 +119,23 @@ class Environment(testmod.Environment):
         self._lock = Lock()
         self.opts = opts
         self.c1 = nfs4client.NFS4Client(opts.server, opts.port, opts.minorversion, secure=opts.secure)
-        s1 = rpc.security.instance(opts.flavor)
-        if opts.flavor == rpc.AUTH_NONE:
-            self.cred1 = s1.init_cred()
-        elif opts.flavor == rpc.AUTH_SYS:
-            self.cred1 = s1.init_cred(uid=opts.uid, gid=opts.gid, name=opts.machinename)
-        elif opts.flavor == rpc.RPCSEC_GSS:
-            call = self.c1.make_call_function(self.c1.c1, 0,
-                                              self.c1.default_prog,
-                                              self.c1.default_vers)
-            krb5_cred = AuthGss().init_cred(call, target="nfs@%s" % opts.server)
-            krb5_cred.service = opts.service
-            self.cred1 = krb5_cred
-        self.c1.set_cred(self.cred1)
+        self.cred1 = self._make_cred(self.c1, opts.server)
         self.cred2 = AuthSys().init_cred(uid=1111, gid=37, name=b"shampoo")
 
         opts.home = opts.path + [b'tmp']
         self.c1.homedir = opts.home
+
+        # Optional second server, used only by inter-server (server-to-server)
+        # COPY tests.  Enabled by passing --server2 SERVER:/PATH on the
+        # command line; None otherwise so single-server runs are unaffected.
+        self.c2 = None
+        if getattr(opts, "server2", None):
+            self.c2 = nfs4client.NFS4Client(opts.server2_host, opts.server2_port,
+                                            opts.minorversion, secure=opts.secure)
+            opts.server2_home = opts.server2_path + [b'tmp']
+            self.c2.homedir = opts.server2_home
+            self._make_cred(self.c2, opts.server2_host)
+
         # Put this after client creation, to ensure _last_verf bigger than
         # any natural client verifiers
         self.timestamp = int(time.time())
@@ -146,38 +146,72 @@ class Environment(testmod.Environment):
         self.stateid1 = stateid4(0xffffffff, b'\xff'*12)
 
         log.info("Created client to %s, %i" % (opts.server, opts.port))
+        if self.c2 is not None:
+            log.info("Created second client to %s, %i" %
+                     (opts.server2_host, opts.server2_port))
+
+    def _make_cred(self, client, server_host):
+        """Build and install an RPC credential on client for server_host."""
+        s = rpc.security.instance(self.opts.flavor)
+        if self.opts.flavor == rpc.AUTH_NONE:
+            cred = s.init_cred()
+        elif self.opts.flavor == rpc.AUTH_SYS:
+            cred = s.init_cred(uid=self.opts.uid, gid=self.opts.gid,
+                               name=self.opts.machinename)
+        elif self.opts.flavor == rpc.RPCSEC_GSS:
+            call = client.make_call_function(client.c1, 0,
+                                             client.default_prog,
+                                             client.default_vers)
+            cred = AuthGss().init_cred(call, target="nfs@%s" % server_host)
+            cred.service = self.opts.service
+        client.set_cred(cred)
+        return cred
+
+    def _all_clients(self):
+        """All configured clients (c1, and c2 if a second server was given)."""
+        return [c for c in (self.c1, self.c2) if c is not None]
 
     def init(self):
         """Run once before any test is run"""
         if self.opts.noinit:
             return
-        sess = self.c1.new_client_session(b"Environment.init_%i" %
-                                                    self.timestamp)
+        self._init_server(self.c1, self.opts.path, self.opts.home)
+        if self.c2 is not None:
+            self._init_server(self.c2, self.opts.server2_path,
+                              self.opts.server2_home)
+        self.clean_sessions()
+        self.clean_clients()
+
+    def _init_server(self, c, path, home):
+        """Prepare (and optionally build) the test tree on a single server."""
+        sess = c.new_client_session(b"Environment.init_%i" % self.timestamp)
         if self.opts.maketree:
-            self._maketree(sess)
-        # Make sure opts.home exists
-        res = sess.compound(use_obj(self.opts.home))
-        check(res, msg="Could not LOOKUP /%s," % b'/'.join(self.opts.home))
+            self._maketree(sess, path, home)
+        # Make sure home exists
+        res = sess.compound(use_obj(home))
+        check(res, msg="Could not LOOKUP /%s," % b'/'.join(home))
         # Make sure it is empty
-        clean_dir(sess, self.opts.home)
+        clean_dir(sess, home)
         sess.c.null()
-        self.clean_sessions()
-        self.clean_clients()
 
-    def _maketree(self, sess):
+    def _maketree(self, sess, path=None, home=None):
         """Make test tree"""
+        if path is None:
+            path = self.opts.path
+        if home is None:
+            home = self.opts.home
         # ensure /tmp (and path leading up) exists
-        path = []
-        for comp in self.opts.home:
-            path.append(comp)
-            res = sess.compound(use_obj(path))
+        p = []
+        for comp in home:
+            p.append(comp)
+            res = sess.compound(use_obj(p))
             check(res, [NFS4_OK, NFS4ERR_NOENT],
-                      "LOOKUP /%s," % b'/'.join(path))
+                      "LOOKUP /%s," % b'/'.join(p))
             if res.status == NFS4ERR_NOENT:
-                res = create_obj(sess, path, NF4DIR)
-                check(res, msg="Trying to create /%s," % b'/'.join(path))
+                res = create_obj(sess, p, NF4DIR)
+                check(res, msg="Trying to create /%s," % b'/'.join(p))
         # ensure /tree exists and is empty
-        tree = self.opts.path + [b'tree']
+        tree = path + [b'tree']
         res = sess.compound(use_obj(tree))
         check(res, [NFS4_OK, NFS4ERR_NOENT])
         if res.status == NFS4ERR_NOENT:
@@ -210,16 +244,21 @@ class Environment(testmod.Environment):
         """Run once after all tests are run"""
         if self.opts.nocleanup:
             return
-        sess = self.c1.new_client_session(b"Environment.init_%i" % self.timestamp)
-        clean_dir(sess, self.opts.home)
-        sess.c.null()
+        homes = [(self.c1, self.opts.home)]
+        if self.c2 is not None:
+            homes.append((self.c2, self.opts.server2_home))
+        for c, home in homes:
+            sess = c.new_client_session(b"Environment.init_%i" % self.timestamp)
+            clean_dir(sess, home)
+            sess.c.null()
         self.clean_sessions()
         self.clean_clients()
 
     def startUp(self):
         """Run before each test"""
         log.debug("Sending pretest NULL")
-        self.c1.null()
+        for c in self._all_clients():
+            c.null()
         log.debug("Got pretest NULL response")
 
     def sleep(self, sec, msg=''):
@@ -260,16 +299,18 @@ class Environment(testmod.Environment):
         return b"%s_%i" % (os.fsencode(t.code), self.timestamp)
 
     def clean_sessions(self):
-        """Destroy client name env.c1"""
-        for sessionid in list(self.c1.sessions):
-            self.c1.compound([op.destroy_session(sessionid)])
-            del(self.c1.sessions[sessionid])
+        """Destroy all sessions on all configured clients"""
+        for c in self._all_clients():
+            for sessionid in list(c.sessions):
+                c.compound([op.destroy_session(sessionid)])
+                del(c.sessions[sessionid])
 
     def clean_clients(self):
-        """Destroy client name env.c1"""
-        for clientid in list(self.c1.clients):
-            self.c1.compound([op.destroy_clientid(clientid)])
-            del(self.c1.clients[clientid])
+        """Destroy all client ids on all configured clients"""
+        for c in self._all_clients():
+            for clientid in list(c.clients):
+                c.compound([op.destroy_clientid(clientid)])
+                del(c.clients[clientid])
 
 #########################################
 debug_fail = False
diff --git a/nfs4.1/testserver.py b/nfs4.1/testserver.py
index 0970c64efe34..f38279b36cba 100755
--- a/nfs4.1/testserver.py
+++ b/nfs4.1/testserver.py
@@ -153,6 +153,13 @@ def scan_options(p):
                  help="Use FH for certain specialized tests")
     p.add_option_group(g)
 
+    g = OptionGroup(p, "Inter-server copy options",
+                    "A second server enables NFSv4.2 inter-server "
+                    "(server-to-server) COPY tests.")
+    g.add_option("--server2", default=None, metavar="SERVER:/PATH",
+                 help="Second server and export for inter-server copy tests")
+    p.add_option_group(g)
+
     g = OptionGroup(p, "Server workaround options",
                     "Certain servers handle certain things in unexpected ways."
                     " These options allow you to alter test behavior so that "
@@ -259,6 +266,13 @@ def main():
 
     opt.server, opt.port = server_list[0]
 
+    # Optional second server for inter-server copy tests
+    if opt.server2 is not None:
+        server2_list, opt.server2_path = nfs4lib.parse_nfs_url(opt.server2)
+        if not server2_list:
+            p.error("%s not a valid server name" % opt.server2)
+        opt.server2_host, opt.server2_port = server2_list[0]
+
     if not args:
         p.error("No tests given")
 

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH pynfs 13/13] server41tests: test inter-server COPY
  2026-07-09 19:02 [PATCH pynfs 00/13] server41tests: add some tests for copy offload Jeff Layton
                   ` (11 preceding siblings ...)
  2026-07-09 19:02 ` [PATCH pynfs 12/13] server41tests: support a second server for inter-server copy Jeff Layton
@ 2026-07-09 19:02 ` Jeff Layton
  12 siblings, 0 replies; 14+ messages in thread
From: Jeff Layton @ 2026-07-09 19:02 UTC (permalink / raw)
  To: Calum Mackay
  Cc: Chuck Lever, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
	linux-nfs, Jeff Layton

Add INTERCOPY1, a full NFSv4.2 server-to-server COPY across two servers.
The source file is created on the second server and the destination on
the primary server; the client issues COPY_NOTIFY to the source, then
COPY to the destination naming the source via ca_source_server, and
verifies the copied data.

The test requires --server2 SERVER:/PATH and is reported as unsupported
when no second server is configured or when either server lacks the
inter-server copy operations.

Signed-off-by: Jeff Layton <jlayton@kernel.org>
---
 nfs4.1/server41tests/st_copy.py | 80 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 80 insertions(+)

diff --git a/nfs4.1/server41tests/st_copy.py b/nfs4.1/server41tests/st_copy.py
index 7eb6e2c98c4d..9cb9c199a025 100644
--- a/nfs4.1/server41tests/st_copy.py
+++ b/nfs4.1/server41tests/st_copy.py
@@ -436,3 +436,83 @@ def testCopyNotifyNoFh(t, env):
         t.fail_support("Server does not support COPY_NOTIFY "
                        "(inter-server copy source)")
     check(res, NFS4ERR_NOFILEHANDLE, msg="COPY_NOTIFY with no filehandle")
+
+def _inter_copy_ops(src_fh, src_stateid, dst_fh, dst_stateid, source_server,
+                    src_offset=0, dst_offset=0, count=0,
+                    consecutive=0, synchronous=1):
+    """COMPOUND for an inter-server COPY, sent to the destination server.
+
+    SAVED_FH is the source file's filehandle as known to the source server;
+    the destination server treats it as opaque and forwards it to the source
+    named by source_server (a non-empty ca_source_server list is what marks
+    the copy as inter-server).
+    """
+    return [op.putfh(src_fh), op.savefh(), op.putfh(dst_fh),
+            op.copy(src_stateid, dst_stateid, src_offset, dst_offset,
+                    count, consecutive, synchronous, source_server)]
+
+def testInterServerCopy(t, env):
+    """server-to-server (inter-server) COPY across two servers
+
+    Requires a second server: pass --server2 SERVER:/PATH.  The source file
+    is created on the second server (env.c2, the copy source) and the
+    destination file on the primary server (env.c1, the copy destination).
+    The client issues COPY_NOTIFY to the source to authorize the destination,
+    then COPY to the destination naming the source via ca_source_server.
+
+    FLAGS: copy
+    CODE: INTERCOPY1
+    VERS: 2-
+    """
+    if env.c2 is None:
+        t.fail_support("No second server configured "
+                       "(pass --server2 SERVER:/PATH to enable)")
+
+    # Source file on the second server (the copy source).
+    src_sess = env.c2.new_client_session(env.testname(t) + b"_src")
+    src_fh, src_stateid = _create_and_open(src_sess, env.testname(t))
+    data = b"inter-server copy payload " * 4096
+    _write_data(src_sess, src_fh, src_stateid, data)
+
+    # Ask the source to authorize the primary server as the copy destination.
+    dest_netloc = _server_netloc(env)   # names the primary server (env.c1)
+    res = src_sess.compound([op.putfh(src_fh),
+                             op.copy_notify(src_stateid, dest_netloc)])
+    check(res, [NFS4_OK, NFS4ERR_NOTSUPP], msg="COPY_NOTIFY on source server")
+    if res.status == NFS4ERR_NOTSUPP:
+        t.fail_support("Source server does not support COPY_NOTIFY")
+    cnr = res.resarray[-1]
+    copy_src_stateid = cnr.cnr_stateid
+    source_server = cnr.cnr_source_server
+    if not source_server:
+        fail("COPY_NOTIFY returned an empty cnr_source_server list")
+
+    # Destination file on the primary server (the copy destination).
+    dst_sess = env.c1.new_client_session(env.testname(t) + b"_dst")
+    dst_fh, dst_stateid = _create_and_open(dst_sess, env.testname(t))
+
+    # COPY to the destination, naming the source via ca_source_server.
+    # Inter-server copy is asynchronous on the Linux server (a synchronous
+    # request returns NFS4ERR_NOTSUPP), so ask for async and poll below.
+    ops = _inter_copy_ops(src_fh, copy_src_stateid, dst_fh, dst_stateid,
+                          source_server, count=len(data), synchronous=0)
+    res = dst_sess.compound(ops)
+    check(res, [NFS4_OK, NFS4ERR_NOTSUPP], msg="inter-server COPY")
+    if res.status == NFS4ERR_NOTSUPP:
+        t.fail_support("Destination server does not support inter-server COPY")
+    cr = res.resarray[-1]
+
+    if cr.cr_resok4.cr_requirements.cr_synchronous:
+        if cr.cr_response.wr_count != len(data):
+            fail("Inter-server copy expected %d bytes, got %d" %
+                 (len(data), cr.cr_response.wr_count))
+    else:
+        copy_stateid = cr.cr_response.wr_callback_id[0]
+        status = _poll_offload_status(dst_sess, dst_fh, copy_stateid)
+        if status.osr_complete[0] != NFS4_OK:
+            fail("Async inter-server copy error: %d" % status.osr_complete[0])
+        if status.osr_count != len(data):
+            fail("Expected %d bytes copied, got %d" %
+                 (len(data), status.osr_count))
+
+    _verify_data(dst_sess, dst_fh, dst_stateid, data)

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 14+ messages in thread

end of thread, other threads:[~2026-07-09 19:02 UTC | newest]

Thread overview: 14+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-09 19:02 [PATCH pynfs 00/13] server41tests: add some tests for copy offload Jeff Layton
2026-07-09 19:02 ` [PATCH pynfs 01/13] server41tests: add helpers and basic synchronous COPY test Jeff Layton
2026-07-09 19:02 ` [PATCH pynfs 02/13] server41tests: test COPY with non-zero offsets Jeff Layton
2026-07-09 19:02 ` [PATCH pynfs 03/13] server41tests: test async COPY with OFFLOAD_STATUS polling Jeff Layton
2026-07-09 19:02 ` [PATCH pynfs 04/13] server41tests: test OFFLOAD_STATUS persists after async copy completes Jeff Layton
2026-07-09 19:02 ` [PATCH pynfs 05/13] server41tests: test OFFLOAD_CANCEL on async copy Jeff Layton
2026-07-09 19:02 ` [PATCH pynfs 06/13] server41tests: test COPY with bad source stateid Jeff Layton
2026-07-09 19:02 ` [PATCH pynfs 07/13] server41tests: test COPY with bad destination stateid Jeff Layton
2026-07-09 19:02 ` [PATCH pynfs 08/13] server41tests: test OFFLOAD_STATUS with fabricated stateid Jeff Layton
2026-07-09 19:02 ` [PATCH pynfs 09/13] server41tests: test COPY within same file Jeff Layton
2026-07-09 19:02 ` [PATCH pynfs 10/13] nfs4.1: fix COPY_NOTIFY args union arm name in XDR Jeff Layton
2026-07-09 19:02 ` [PATCH pynfs 11/13] server41tests: test COPY_NOTIFY Jeff Layton
2026-07-09 19:02 ` [PATCH pynfs 12/13] server41tests: support a second server for inter-server copy Jeff Layton
2026-07-09 19:02 ` [PATCH pynfs 13/13] server41tests: test inter-server COPY Jeff Layton

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.