Files
2026-06-10 09:29:09 +02:00

301 lines
14 KiB
Python

from basetest import *
class TestZfsAutobackupClone(unittest2.TestCase):
"""ZFS clone replication: source datasets that are themselves clones get
replicated to the target as clones of the corresponding target-side origin
snapshot, via 'zfs send -i <origin_snap> <clone>@<snap>'."""
def setUp(self):
prepare_zpools()
self.longMessage = True
def test_clone_happy_path(self):
"""Single run, both origin and clone selected. The origin snapshot is
auto-included because a clone in the selection depends on it, so the user
doesn't need --other-snapshots."""
shelltest("zfs snapshot test_source1/fs1@base")
shelltest("zfs clone test_source1/fs1@base test_source1/fs1_clone")
shelltest("zfs set autobackup:test=true test_source1/fs1_clone")
before = shelltest("zfs list -H -o name -r -t snapshot " + TEST_POOLS)
with OutputIO() as buf_out, OutputIO() as buf_err:
with redirect_stdout(buf_out), redirect_stderr(buf_err):
with mocktime("20101111000000"):
self.assertFalse(ZfsAutobackup(
"test test_target1 --no-progress --verbose --test".split(" ")).run())
combined = buf_out.getvalue() + buf_err.getvalue()
print(combined)
self.assertNotIn("guid", combined) # testing can give guid errors
self.assertNotIn("STDERR", combined)
after = shelltest("zfs list -H -o name -r -t snapshot " + TEST_POOLS)
self.assertMultiLineEqual(before, after)
with mocktime("20101111000000"):
self.assertFalse(ZfsAutobackup(
"test test_target1 --no-progress --verbose".split(" ")).run())
r = shelltest("zfs list -H -o name -r -t snapshot,filesystem " + TEST_POOLS)
self.assertMultiLineEqual(r, """
test_source1
test_source1/fs1
test_source1/fs1@base
test_source1/fs1@test-20101111000000
test_source1/fs1/sub
test_source1/fs1/sub@test-20101111000000
test_source1/fs1_clone
test_source1/fs1_clone@test-20101111000000
test_source2
test_source2/fs2
test_source2/fs2/sub
test_source2/fs2/sub@test-20101111000000
test_source2/fs3
test_source2/fs3/sub
test_target1
test_target1/test_source1
test_target1/test_source1/fs1
test_target1/test_source1/fs1@base
test_target1/test_source1/fs1@test-20101111000000
test_target1/test_source1/fs1/sub
test_target1/test_source1/fs1/sub@test-20101111000000
test_target1/test_source1/fs1_clone
test_target1/test_source1/fs1_clone@test-20101111000000
test_target1/test_source2
test_target1/test_source2/fs2
test_target1/test_source2/fs2/sub
test_target1/test_source2/fs2/sub@test-20101111000000
""")
origin = shelltest("zfs get -H -o value origin test_target1/test_source1/fs1_clone").strip()
self.assertEqual(origin, "test_target1/test_source1/fs1@base")
def test_clone_origin_not_on_target_falls_back_to_full_send(self):
"""If the clone's origin isn't replicated, warn and fall back to full send."""
shelltest("zfs inherit autobackup:test test_source1/fs1")
shelltest("zfs snapshot test_source1/fs1@base")
shelltest("zfs clone test_source1/fs1@base test_source1/fs1_clone")
shelltest("zfs set autobackup:test=true test_source1/fs1_clone")
before = shelltest("zfs list -H -o name -r -t snapshot " + TEST_POOLS)
with OutputIO() as buf_out, OutputIO() as buf_err:
with redirect_stdout(buf_out), redirect_stderr(buf_err):
with mocktime("20101111000000"):
self.assertFalse(ZfsAutobackup(
"test test_target1 --no-progress --verbose --test".split(" ")).run())
combined = buf_out.getvalue() + buf_err.getvalue()
print(combined)
self.assertIn("Cannot replicate as clone", combined)
self.assertNotIn("STDERR", combined)
after = shelltest("zfs list -H -o name -r -t snapshot " + TEST_POOLS)
self.assertMultiLineEqual(before, after)
with OutputIO() as buf_out, OutputIO() as buf_err:
with redirect_stdout(buf_out), redirect_stderr(buf_err):
with mocktime("20101111000000"):
self.assertFalse(ZfsAutobackup(
"test test_target1 --no-progress --verbose".split(" ")).run())
combined = buf_out.getvalue() + buf_err.getvalue()
print(combined)
self.assertIn("Cannot replicate as clone", combined)
self.assertNotIn("STDERR", combined)
origin = shelltest("zfs get -H -o value origin test_target1/test_source1/fs1_clone").strip()
self.assertEqual(origin, "-")
def test_clone_after_initial_origin_backup(self):
"""First run backs up the origin; second run replicates a clone of it."""
with mocktime("20101111000000"):
self.assertFalse(ZfsAutobackup(
"test test_target1 --no-progress --verbose".split(" ")).run())
shelltest("zfs clone test_source1/fs1@test-20101111000000 test_source1/fs1_clone")
shelltest("zfs set autobackup:test=true test_source1/fs1_clone")
before = shelltest("zfs list -H -o name -r -t snapshot " + TEST_POOLS)
with OutputIO() as buf_out, OutputIO() as buf_err:
with redirect_stdout(buf_out), redirect_stderr(buf_err):
with mocktime("20101111000001"):
self.assertFalse(ZfsAutobackup(
"test test_target1 --no-progress --verbose --allow-empty --test".split(" ")).run())
combined = buf_out.getvalue() + buf_err.getvalue()
print(combined)
self.assertNotIn("STDERR", combined)
after = shelltest("zfs list -H -o name -r -t snapshot " + TEST_POOLS)
self.assertMultiLineEqual(before, after)
with mocktime("20101111000001"):
self.assertFalse(ZfsAutobackup(
"test test_target1 --no-progress --verbose --allow-empty".split(" ")).run())
origin = shelltest("zfs get -H -o value origin test_target1/test_source1/fs1_clone").strip()
self.assertEqual(origin, "test_target1/test_source1/fs1@test-20101111000000")
def test_no_clone_flag_disables_clone_replication(self):
"""--no-clone reverts to the previous full-send behaviour for clones."""
shelltest("zfs snapshot test_source1/fs1@base")
shelltest("zfs clone test_source1/fs1@base test_source1/fs1_clone")
shelltest("zfs set autobackup:test=true test_source1/fs1_clone")
before = shelltest("zfs list -H -o name -r -t snapshot " + TEST_POOLS)
with OutputIO() as buf_out, OutputIO() as buf_err:
with redirect_stdout(buf_out), redirect_stderr(buf_err):
with mocktime("20101111000000"):
self.assertFalse(ZfsAutobackup(
"test test_target1 --no-progress --verbose --no-clone --test".split(" ")).run())
combined = buf_out.getvalue() + buf_err.getvalue()
print(combined)
self.assertNotIn("STDERR", combined)
after = shelltest("zfs list -H -o name -r -t snapshot " + TEST_POOLS)
self.assertMultiLineEqual(before, after)
with mocktime("20101111000000"):
self.assertFalse(ZfsAutobackup(
"test test_target1 --no-progress --verbose --no-clone".split(" ")).run())
origin = shelltest("zfs get -H -o value origin test_target1/test_source1/fs1_clone").strip()
self.assertEqual(origin, "-")
def test_subsequent_incremental_after_clone_send(self):
"""A second run after a clone has been sent is a plain incremental."""
shelltest("zfs snapshot test_source1/fs1@base")
shelltest("zfs clone test_source1/fs1@base test_source1/fs1_clone")
shelltest("zfs set autobackup:test=true test_source1/fs1_clone")
with mocktime("20101111000000"):
self.assertFalse(ZfsAutobackup(
"test test_target1 --no-progress --verbose".split(" ")).run())
before = shelltest("zfs list -H -o name -r -t snapshot " + TEST_POOLS)
with OutputIO() as buf_out, OutputIO() as buf_err:
with redirect_stdout(buf_out), redirect_stderr(buf_err):
with mocktime("20101111000001"):
self.assertFalse(ZfsAutobackup(
"test test_target1 --no-progress --verbose --allow-empty --test".split(" ")).run())
combined = buf_out.getvalue() + buf_err.getvalue()
print(combined)
self.assertNotIn("STDERR", combined)
after = shelltest("zfs list -H -o name -r -t snapshot " + TEST_POOLS)
self.assertMultiLineEqual(before, after)
with mocktime("20101111000001"):
self.assertFalse(ZfsAutobackup(
"test test_target1 --no-progress --verbose --allow-empty".split(" ")).run())
r = shelltest("zfs list -H -o name -t snapshot test_target1/test_source1/fs1_clone")
self.assertMultiLineEqual(r, """
test_target1/test_source1/fs1_clone@test-20101111000000
test_target1/test_source1/fs1_clone@test-20101111000001
""")
origin = shelltest("zfs get -H -o value origin test_target1/test_source1/fs1_clone").strip()
self.assertEqual(origin, "test_target1/test_source1/fs1@base")
def test_topological_sort_processes_origin_before_clone(self):
"""When the clone alphabetically precedes its origin, the topo sort still
processes the origin first so the clone can pin to its target origin."""
shelltest("zfs snapshot test_source1/fs1@base")
shelltest("zfs clone test_source1/fs1@base test_source1/aaa_clone")
shelltest("zfs set autobackup:test=true test_source1/aaa_clone")
before = shelltest("zfs list -H -o name -r -t snapshot " + TEST_POOLS)
with OutputIO() as buf_out, OutputIO() as buf_err:
with redirect_stdout(buf_out), redirect_stderr(buf_err):
with mocktime("20101111000000"):
self.assertFalse(ZfsAutobackup(
"test test_target1 --no-progress --verbose --test".split(" ")).run())
combined = buf_out.getvalue() + buf_err.getvalue()
print(combined)
self.assertNotIn("STDERR", combined)
after = shelltest("zfs list -H -o name -r -t snapshot " + TEST_POOLS)
self.assertMultiLineEqual(before, after)
with mocktime("20101111000000"):
self.assertFalse(ZfsAutobackup(
"test test_target1 --no-progress --verbose".split(" ")).run())
origin = shelltest("zfs get -H -o value origin test_target1/test_source1/aaa_clone").strip()
self.assertEqual(origin, "test_target1/test_source1/fs1@base")
def test_test_mode_makes_no_changes_for_clones(self):
"""--test must not modify anything, even when clone handling is exercised."""
shelltest("zfs snapshot test_source1/fs1@base")
shelltest("zfs clone test_source1/fs1@base test_source1/fs1_clone")
shelltest("zfs set autobackup:test=true test_source1/fs1_clone")
with OutputIO() as buf_out, OutputIO() as buf_err:
with redirect_stdout(buf_out), redirect_stderr(buf_err):
with mocktime("20101111000000"):
self.assertFalse(ZfsAutobackup(
"test test_target1 --no-progress --verbose --test".split(" ")).run())
combined = buf_out.getvalue() + buf_err.getvalue()
print(combined)
self.assertNotIn("STDERR", combined)
r = shelltest("zfs list -H -o name -r -t snapshot,filesystem test_target1")
self.assertMultiLineEqual(r, """
test_target1
""")
def test_promoted_clone_origin_on_namespace_descendant_warns_and_falls_back(self):
"""After 'zfs promote', the original parent becomes a clone whose origin lives
on one of its own namespace descendants. zfs recv can't land a clone-creating
stream on top of the placeholder we have to create for that descendant, so we
detect this upfront, warn, and fall back to a full send (which will itself
fail at the placeholder — user can destroy target and rerun, or promote the
source dataset to flip the lineage back)."""
shelltest("zfs snapshot test_source1/fs1@s1")
shelltest("zfs clone test_source1/fs1@s1 test_source1/fs1/inner_clone")
shelltest("zfs promote test_source1/fs1/inner_clone")
origin = shelltest("zfs get -H -o value origin test_source1/fs1").strip()
self.assertEqual(origin, "test_source1/fs1/inner_clone@s1")
before = shelltest("zfs list -H -o name -r -t snapshot " + TEST_POOLS)
with OutputIO() as buf_out, OutputIO() as buf_err:
with redirect_stdout(buf_out), redirect_stderr(buf_err):
with mocktime("20101111000000"):
result = ZfsAutobackup(
"test test_target1 --no-progress --verbose --test -F".split(" ")).run()
combined = buf_out.getvalue() + buf_err.getvalue()
print(combined)
self.assertFalse(result)
self.assertIn("lives on a namespace descendant", combined)
self.assertNotIn("STDERR", combined)
after = shelltest("zfs list -H -o name -r -t snapshot " + TEST_POOLS)
self.assertMultiLineEqual(before, after)
with OutputIO() as buf_out, OutputIO() as buf_err:
with redirect_stdout(buf_out), redirect_stderr(buf_err):
with mocktime("20101111000000"):
result = ZfsAutobackup(
"test test_target1 --no-progress --verbose -F".split(" ")).run()
combined = buf_out.getvalue() + buf_err.getvalue()
print(combined)
self.assertFalse(result)
self.assertIn("lives on a namespace descendant", combined)