mirror of
https://git.proxmox.com/git/mirror_zfs.git
synced 2026-05-22 02:27:36 +03:00
Implement bookmark copying
This feature allows copying existing bookmarks using
zfs bookmark fs#target fs#newbookmark
There are some niche use cases for such functionality,
e.g. when using bookmarks as markers for replication progress.
Copying redaction bookmarks produces a normal bookmark that
cannot be used for redacted send (we are not duplicating
the redaction object).
ZCP support for bookmarking (both creation and copying) will be
implemented in a separate patch based on this work.
Overview:
- Terminology:
- source = existing snapshot or bookmark
- new/bmark = new bookmark
- Implement bookmark copying in `dsl_bookmark.c`
- create new bookmark node
- copy source's `zbn_phys` to new's `zbn_phys`
- zero-out redaction object id in copy
- Extend existing bookmark ioctl nvlist schema to accept
bookmarks as sources
- => `dsl_bookmark_create_nvl_validate` is authoritative
- use `dsl_dataset_is_before` check for both snapshot
and bookmark sources
- Adjust CLI
- refactor shortname expansion logic in `zfs_do_bookmark`
- Update man pages
- warn about redaction bookmark handling
- Add test cases
- CLI
- pyyzfs libzfs_core bindings
Reviewed-by: Matt Ahrens <matt@delphix.com>
Reviewed-by: Paul Dagnelie <pcd@delphix.com>
Reviewed-by: Brian Behlendorf <behlendorf1@llnl.gov>
Signed-off-by: Christian Schwarz <me@cschwarz.com>
Closes #9571
This commit is contained in:
committed by
Brian Behlendorf
parent
7b49bbc816
commit
a73f361fdb
@@ -22,11 +22,15 @@ from __future__ import absolute_import, division, print_function
|
||||
|
||||
|
||||
# https://stackoverflow.com/a/1695250
|
||||
def enum(*sequential, **named):
|
||||
enums = dict(((b, a) for a, b in enumerate(sequential)), **named)
|
||||
def enum_with_offset(offset, sequential, named):
|
||||
enums = dict(((b, a + offset) for a, b in enumerate(sequential)), **named)
|
||||
return type('Enum', (), enums)
|
||||
|
||||
|
||||
def enum(*sequential, **named):
|
||||
return enum_with_offset(0, sequential, named)
|
||||
|
||||
|
||||
#: Maximum length of any ZFS name.
|
||||
MAXNAMELEN = 255
|
||||
#: Default channel program limits
|
||||
@@ -60,12 +64,34 @@ zio_encrypt = enum(
|
||||
'ZIO_CRYPT_AES_256_GCM'
|
||||
)
|
||||
# ZFS-specific error codes
|
||||
ZFS_ERR_CHECKPOINT_EXISTS = 1024
|
||||
ZFS_ERR_DISCARDING_CHECKPOINT = 1025
|
||||
ZFS_ERR_NO_CHECKPOINT = 1026
|
||||
ZFS_ERR_DEVRM_IN_PROGRESS = 1027
|
||||
ZFS_ERR_VDEV_TOO_BIG = 1028
|
||||
ZFS_ERR_WRONG_PARENT = 1033
|
||||
zfs_errno = enum_with_offset(1024, [
|
||||
'ZFS_ERR_CHECKPOINT_EXISTS',
|
||||
'ZFS_ERR_DISCARDING_CHECKPOINT',
|
||||
'ZFS_ERR_NO_CHECKPOINT',
|
||||
'ZFS_ERR_DEVRM_IN_PROGRESS',
|
||||
'ZFS_ERR_VDEV_TOO_BIG',
|
||||
'ZFS_ERR_IOC_CMD_UNAVAIL',
|
||||
'ZFS_ERR_IOC_ARG_UNAVAIL',
|
||||
'ZFS_ERR_IOC_ARG_REQUIRED',
|
||||
'ZFS_ERR_IOC_ARG_BADTYPE',
|
||||
'ZFS_ERR_WRONG_PARENT',
|
||||
'ZFS_ERR_FROM_IVSET_GUID_MISSING',
|
||||
'ZFS_ERR_FROM_IVSET_GUID_MISMATCH',
|
||||
'ZFS_ERR_SPILL_BLOCK_FLAG_MISSING',
|
||||
'ZFS_ERR_UNKNOWN_SEND_STREAM_FEATURE',
|
||||
'ZFS_ERR_EXPORT_IN_PROGRESS',
|
||||
'ZFS_ERR_BOOKMARK_SOURCE_NOT_ANCESTOR',
|
||||
],
|
||||
{}
|
||||
)
|
||||
# compat before we used the enum helper for these values
|
||||
ZFS_ERR_CHECKPOINT_EXISTS = zfs_errno.ZFS_ERR_CHECKPOINT_EXISTS
|
||||
assert(ZFS_ERR_CHECKPOINT_EXISTS == 1024)
|
||||
ZFS_ERR_DISCARDING_CHECKPOINT = zfs_errno.ZFS_ERR_DISCARDING_CHECKPOINT
|
||||
ZFS_ERR_NO_CHECKPOINT = zfs_errno.ZFS_ERR_NO_CHECKPOINT
|
||||
ZFS_ERR_DEVRM_IN_PROGRESS = zfs_errno.ZFS_ERR_DEVRM_IN_PROGRESS
|
||||
ZFS_ERR_VDEV_TOO_BIG = zfs_errno.ZFS_ERR_VDEV_TOO_BIG
|
||||
ZFS_ERR_WRONG_PARENT = zfs_errno.ZFS_ERR_WRONG_PARENT
|
||||
|
||||
|
||||
# vim: softtabstop=4 tabstop=4 expandtab shiftwidth=4
|
||||
|
||||
@@ -39,7 +39,8 @@ from ._constants import (
|
||||
ZFS_ERR_NO_CHECKPOINT,
|
||||
ZFS_ERR_DEVRM_IN_PROGRESS,
|
||||
ZFS_ERR_VDEV_TOO_BIG,
|
||||
ZFS_ERR_WRONG_PARENT
|
||||
ZFS_ERR_WRONG_PARENT,
|
||||
zfs_errno
|
||||
)
|
||||
|
||||
|
||||
@@ -147,21 +148,36 @@ def lzc_destroy_snaps_translate_errors(ret, errlist, snaps, defer):
|
||||
|
||||
|
||||
def lzc_bookmark_translate_errors(ret, errlist, bookmarks):
|
||||
|
||||
if ret == 0:
|
||||
return
|
||||
|
||||
def _map(ret, name):
|
||||
source = bookmarks[name]
|
||||
if ret == errno.EINVAL:
|
||||
if name:
|
||||
snap = bookmarks[name]
|
||||
pool_names = map(_pool_name, bookmarks.keys())
|
||||
if not _is_valid_bmark_name(name):
|
||||
return lzc_exc.BookmarkNameInvalid(name)
|
||||
elif not _is_valid_snap_name(snap):
|
||||
return lzc_exc.SnapshotNameInvalid(snap)
|
||||
elif _fs_name(name) != _fs_name(snap):
|
||||
return lzc_exc.BookmarkMismatch(name)
|
||||
elif any(x != _pool_name(name) for x in pool_names):
|
||||
|
||||
# use _validate* functions for MAXNAMELEN check
|
||||
try:
|
||||
_validate_bmark_name(name)
|
||||
except lzc_exc.ZFSError as e:
|
||||
return e
|
||||
|
||||
try:
|
||||
_validate_snap_name(source)
|
||||
source_is_snap = True
|
||||
except lzc_exc.ZFSError:
|
||||
source_is_snap = False
|
||||
try:
|
||||
_validate_bmark_name(source)
|
||||
source_is_bmark = True
|
||||
except lzc_exc.ZFSError:
|
||||
source_is_bmark = False
|
||||
if not source_is_snap and not source_is_bmark:
|
||||
return lzc_exc.BookmarkSourceInvalid(source)
|
||||
|
||||
if any(x != _pool_name(name) for x in pool_names):
|
||||
return lzc_exc.PoolsDiffer(name)
|
||||
else:
|
||||
invalid_names = [
|
||||
@@ -174,6 +190,8 @@ def lzc_bookmark_translate_errors(ret, errlist, bookmarks):
|
||||
return lzc_exc.SnapshotNotFound(name)
|
||||
if ret == errno.ENOTSUP:
|
||||
return lzc_exc.BookmarkNotSupported(name)
|
||||
if ret == zfs_errno.ZFS_ERR_BOOKMARK_SOURCE_NOT_ANCESTOR:
|
||||
return lzc_exc.BookmarkMismatch(source)
|
||||
return _generic_exception(ret, name, "Failed to create bookmark")
|
||||
|
||||
_handle_err_list(
|
||||
|
||||
@@ -319,14 +319,15 @@ def lzc_bookmark(bookmarks):
|
||||
Create bookmarks.
|
||||
|
||||
:param bookmarks: a dict that maps names of wanted bookmarks to names of
|
||||
existing snapshots.
|
||||
existing snapshots or bookmarks.
|
||||
:type bookmarks: dict of bytes to bytes
|
||||
:raises BookmarkFailure: if any of the bookmarks can not be created for any
|
||||
reason.
|
||||
|
||||
The bookmarks `dict` maps from name of the bookmark
|
||||
(e.g. :file:`{pool}/{fs}#{bmark}`) to the name of the snapshot
|
||||
(e.g. :file:`{pool}/{fs}@{snap}`). All the bookmarks and snapshots must
|
||||
(e.g. :file:`{pool}/{fs}@{snap}`) or existint bookmark
|
||||
:file:`{pool}/{fs}@{snap}`. All the bookmarks and snapshots must
|
||||
be in the same pool.
|
||||
'''
|
||||
errlist = {}
|
||||
|
||||
@@ -227,7 +227,15 @@ class BookmarkNotFound(ZFSError):
|
||||
|
||||
class BookmarkMismatch(ZFSError):
|
||||
errno = errno.EINVAL
|
||||
message = "Bookmark is not in snapshot's filesystem"
|
||||
message = "source is not an ancestor of the new bookmark's dataset"
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
|
||||
class BookmarkSourceInvalid(ZFSError):
|
||||
errno = errno.EINVAL
|
||||
message = "Bookmark source is not a valid snapshot or existing bookmark"
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
@@ -1032,17 +1032,37 @@ class ZFSTest(unittest.TestCase):
|
||||
bmarks = [ZFSTest.pool.makeName(
|
||||
b'fs1#bmark1'), ZFSTest.pool.makeName(b'fs2#bmark1')]
|
||||
bmark_dict = {x: y for x, y in zip(bmarks, snaps)}
|
||||
|
||||
lzc.lzc_snapshot(snaps)
|
||||
lzc.lzc_bookmark(bmark_dict)
|
||||
lzc.lzc_destroy_snaps(snaps, defer=False)
|
||||
|
||||
@skipUnlessBookmarksSupported
|
||||
def test_bookmark_copying(self):
|
||||
snaps = [ZFSTest.pool.makeName(s) for s in [
|
||||
b'fs1@snap1', b'fs1@snap2', b'fs2@snap1']]
|
||||
bmarks = [ZFSTest.pool.makeName(x) for x in [
|
||||
b'fs1#bmark1', b'fs1#bmark2', b'fs2#bmark1']]
|
||||
bmarks_copies = [ZFSTest.pool.makeName(x) for x in [
|
||||
b'fs1#bmark1_copy', b'fs1#bmark2_copy', b'fs2#bmark1_copy']]
|
||||
bmark_dict = {x: y for x, y in zip(bmarks, snaps)}
|
||||
bmark_copies_dict = {x: y for x, y in zip(bmarks_copies, bmarks)}
|
||||
|
||||
for snap in snaps:
|
||||
lzc.lzc_snapshot([snap])
|
||||
lzc.lzc_bookmark(bmark_dict)
|
||||
|
||||
lzc.lzc_bookmark(bmark_copies_dict)
|
||||
lzc.lzc_destroy_bookmarks(bmarks_copies)
|
||||
|
||||
lzc.lzc_destroy_bookmarks(bmarks)
|
||||
lzc.lzc_destroy_snaps(snaps, defer=False)
|
||||
|
||||
@skipUnlessBookmarksSupported
|
||||
def test_bookmarks_empty(self):
|
||||
lzc.lzc_bookmark({})
|
||||
|
||||
@skipUnlessBookmarksSupported
|
||||
def test_bookmarks_mismatching_name(self):
|
||||
def test_bookmarks_foregin_source(self):
|
||||
snaps = [ZFSTest.pool.makeName(b'fs1@snap1')]
|
||||
bmarks = [ZFSTest.pool.makeName(b'fs2#bmark1')]
|
||||
bmark_dict = {x: y for x, y in zip(bmarks, snaps)}
|
||||
@@ -1107,7 +1127,7 @@ class ZFSTest(unittest.TestCase):
|
||||
self.assertIsInstance(e, lzc_exc.NameTooLong)
|
||||
|
||||
@skipUnlessBookmarksSupported
|
||||
def test_bookmarks_mismatching_names(self):
|
||||
def test_bookmarks_foreign_sources(self):
|
||||
snaps = [ZFSTest.pool.makeName(
|
||||
b'fs1@snap1'), ZFSTest.pool.makeName(b'fs2@snap1')]
|
||||
bmarks = [ZFSTest.pool.makeName(
|
||||
@@ -1122,7 +1142,7 @@ class ZFSTest(unittest.TestCase):
|
||||
self.assertIsInstance(e, lzc_exc.BookmarkMismatch)
|
||||
|
||||
@skipUnlessBookmarksSupported
|
||||
def test_bookmarks_partially_mismatching_names(self):
|
||||
def test_bookmarks_partially_foreign_sources(self):
|
||||
snaps = [ZFSTest.pool.makeName(
|
||||
b'fs1@snap1'), ZFSTest.pool.makeName(b'fs2@snap1')]
|
||||
bmarks = [ZFSTest.pool.makeName(
|
||||
@@ -1154,33 +1174,48 @@ class ZFSTest(unittest.TestCase):
|
||||
|
||||
@skipUnlessBookmarksSupported
|
||||
def test_bookmarks_missing_snap(self):
|
||||
fss = [ZFSTest.pool.makeName(b'fs1'), ZFSTest.pool.makeName(b'fs2')]
|
||||
snaps = [ZFSTest.pool.makeName(
|
||||
b'fs1@snap1'), ZFSTest.pool.makeName(b'fs2@snap1')]
|
||||
bmarks = [ZFSTest.pool.makeName(
|
||||
b'fs1#bmark1'), ZFSTest.pool.makeName(b'fs2#bmark1')]
|
||||
bmark_dict = {x: y for x, y in zip(bmarks, snaps)}
|
||||
|
||||
lzc.lzc_snapshot(snaps[0:1])
|
||||
lzc.lzc_snapshot(snaps[0:1]) # only create fs1@snap1
|
||||
|
||||
with self.assertRaises(lzc_exc.BookmarkFailure) as ctx:
|
||||
lzc.lzc_bookmark(bmark_dict)
|
||||
|
||||
for e in ctx.exception.errors:
|
||||
self.assertIsInstance(e, lzc_exc.SnapshotNotFound)
|
||||
|
||||
# no new bookmarks are created if one or more sources do not exist
|
||||
for fs in fss:
|
||||
fsbmarks = lzc.lzc_get_bookmarks(fs)
|
||||
self.assertEqual(len(fsbmarks), 0)
|
||||
|
||||
@skipUnlessBookmarksSupported
|
||||
def test_bookmarks_missing_snaps(self):
|
||||
fss = [ZFSTest.pool.makeName(b'fs1'), ZFSTest.pool.makeName(b'fs2')]
|
||||
snaps = [ZFSTest.pool.makeName(
|
||||
b'fs1@snap1'), ZFSTest.pool.makeName(b'fs2@snap1')]
|
||||
bmarks = [ZFSTest.pool.makeName(
|
||||
b'fs1#bmark1'), ZFSTest.pool.makeName(b'fs2#bmark1')]
|
||||
bmark_dict = {x: y for x, y in zip(bmarks, snaps)}
|
||||
|
||||
# do not create any snapshots
|
||||
|
||||
with self.assertRaises(lzc_exc.BookmarkFailure) as ctx:
|
||||
lzc.lzc_bookmark(bmark_dict)
|
||||
|
||||
for e in ctx.exception.errors:
|
||||
self.assertIsInstance(e, lzc_exc.SnapshotNotFound)
|
||||
|
||||
# no new bookmarks are created if one or more sources do not exist
|
||||
for fs in fss:
|
||||
fsbmarks = lzc.lzc_get_bookmarks(fs)
|
||||
self.assertEqual(len(fsbmarks), 0)
|
||||
|
||||
@skipUnlessBookmarksSupported
|
||||
def test_bookmarks_for_the_same_snap(self):
|
||||
snap = ZFSTest.pool.makeName(b'fs1@snap1')
|
||||
|
||||
Reference in New Issue
Block a user