From 35cbff76c9360bd452e14cd383bb9783842d7934 Mon Sep 17 00:00:00 2001 From: Alexander Motin Date: Tue, 15 Apr 2025 16:07:05 -0400 Subject: [PATCH] Introduce zfs rewrite subcommand This allows to rewrite content of specified file(s) as-is without modifications, but at a different location, compression, checksum, dedup, copies and other parameter values. It is faster than read plus write, since it does not require data copying to user-space. It is also faster for sync=always datasets, since without data modification it does not require ZIL writing. Also since it is protected by normal range range locks, it can be done under any other load. Also it does not affect file's modification time or other properties. Signed-off-by: Alexander Motin Sponsored by: iXsystems, Inc. --- cmd/zfs/zfs_main.c | 204 +++++++++++++++++- contrib/debian/openzfs-zfsutils.install | 1 + include/sys/fs/zfs.h | 9 + include/sys/zfs_vnops.h | 1 + man/Makefile.am | 1 + man/man8/zfs-rewrite.8 | 59 +++++ man/man8/zfs.8 | 8 +- module/os/freebsd/zfs/zfs_vnops_os.c | 12 ++ module/os/linux/zfs/zpl_file.c | 23 ++ module/zfs/zfs_vnops.c | 137 ++++++++++++ tests/runfiles/common.run | 4 + tests/runfiles/sanity.run | 4 + tests/zfs-tests/tests/Makefile.am | 3 + .../cli_root/zfs_rewrite/cleanup.ksh | 26 +++ .../functional/cli_root/zfs_rewrite/setup.ksh | 28 +++ .../cli_root/zfs_rewrite/zfs_rewrite.ksh | 104 +++++++++ 16 files changed, 619 insertions(+), 5 deletions(-) create mode 100644 man/man8/zfs-rewrite.8 create mode 100755 tests/zfs-tests/tests/functional/cli_root/zfs_rewrite/cleanup.ksh create mode 100755 tests/zfs-tests/tests/functional/cli_root/zfs_rewrite/setup.ksh create mode 100755 tests/zfs-tests/tests/functional/cli_root/zfs_rewrite/zfs_rewrite.ksh diff --git a/cmd/zfs/zfs_main.c b/cmd/zfs/zfs_main.c index d6935d103ca8..c6eb2f43ef8f 100644 --- a/cmd/zfs/zfs_main.c +++ b/cmd/zfs/zfs_main.c @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -121,6 +122,7 @@ static int zfs_do_change_key(int argc, char **argv); static int zfs_do_project(int argc, char **argv); static int zfs_do_version(int argc, char **argv); static int zfs_do_redact(int argc, char **argv); +static int zfs_do_rewrite(int argc, char **argv); static int zfs_do_wait(int argc, char **argv); #ifdef __FreeBSD__ @@ -193,6 +195,7 @@ typedef enum { HELP_CHANGE_KEY, HELP_VERSION, HELP_REDACT, + HELP_REWRITE, HELP_JAIL, HELP_UNJAIL, HELP_WAIT, @@ -227,7 +230,7 @@ static zfs_command_t command_table[] = { { "promote", zfs_do_promote, HELP_PROMOTE }, { "rename", zfs_do_rename, HELP_RENAME }, { "bookmark", zfs_do_bookmark, HELP_BOOKMARK }, - { "program", zfs_do_channel_program, HELP_CHANNEL_PROGRAM }, + { "diff", zfs_do_diff, HELP_DIFF }, { NULL }, { "list", zfs_do_list, HELP_LIST }, { NULL }, @@ -249,27 +252,31 @@ static zfs_command_t command_table[] = { { NULL }, { "send", zfs_do_send, HELP_SEND }, { "receive", zfs_do_receive, HELP_RECEIVE }, + { "redact", zfs_do_redact, HELP_REDACT }, { NULL }, { "allow", zfs_do_allow, HELP_ALLOW }, - { NULL }, { "unallow", zfs_do_unallow, HELP_UNALLOW }, { NULL }, { "hold", zfs_do_hold, HELP_HOLD }, { "holds", zfs_do_holds, HELP_HOLDS }, { "release", zfs_do_release, HELP_RELEASE }, - { "diff", zfs_do_diff, HELP_DIFF }, + { NULL }, { "load-key", zfs_do_load_key, HELP_LOAD_KEY }, { "unload-key", zfs_do_unload_key, HELP_UNLOAD_KEY }, { "change-key", zfs_do_change_key, HELP_CHANGE_KEY }, - { "redact", zfs_do_redact, HELP_REDACT }, + { NULL }, + { "program", zfs_do_channel_program, HELP_CHANNEL_PROGRAM }, + { "rewrite", zfs_do_rewrite, HELP_REWRITE }, { "wait", zfs_do_wait, HELP_WAIT }, #ifdef __FreeBSD__ + { NULL }, { "jail", zfs_do_jail, HELP_JAIL }, { "unjail", zfs_do_unjail, HELP_UNJAIL }, #endif #ifdef __linux__ + { NULL }, { "zone", zfs_do_zone, HELP_ZONE }, { "unzone", zfs_do_unzone, HELP_UNZONE }, #endif @@ -432,6 +439,9 @@ get_usage(zfs_help_t idx) case HELP_REDACT: return (gettext("\tredact " " ...\n")); + case HELP_REWRITE: + return (gettext("\trewrite [-rvx] [-o ] [-l ] " + "\n")); case HELP_JAIL: return (gettext("\tjail \n")); case HELP_UNJAIL: @@ -9016,6 +9026,192 @@ zfs_do_project(int argc, char **argv) return (ret); } +static int +zfs_rewrite_file(const char *path, boolean_t verbose, zfs_rewrite_args_t *args) +{ + int fd, ret = 0; + + fd = open(path, O_WRONLY); + if (fd < 0) { + ret = errno; + (void) fprintf(stderr, gettext("failed to open %s: %s\n"), + path, strerror(errno)); + return (ret); + } + + if (ioctl(fd, ZFS_IOC_REWRITE, args) < 0) { + ret = errno; + (void) fprintf(stderr, gettext("failed to rewrite %s: %s\n"), + path, strerror(errno)); + } else if (verbose) { + printf("%s\n", path); + } + + close(fd); + return (ret); +} + +static int +zfs_rewrite_dir(const char *path, boolean_t verbose, boolean_t xdev, dev_t dev, + zfs_rewrite_args_t *args, nvlist_t *dirs) +{ + struct dirent *ent; + DIR *dir; + int ret = 0, err; + + dir = opendir(path); + if (dir == NULL) { + if (errno == ENOENT) + return (0); + ret = errno; + (void) fprintf(stderr, gettext("failed to opendir %s: %s\n"), + path, strerror(errno)); + return (ret); + } + + size_t plen = strlen(path) + 1; + while ((ent = readdir(dir)) != NULL) { + char *fullname; + struct stat st; + + if (ent->d_type != DT_REG && ent->d_type != DT_DIR) + continue; + + if (strcmp(ent->d_name, ".") == 0 || + strcmp(ent->d_name, "..") == 0) + continue; + + if (plen + strlen(ent->d_name) >= PATH_MAX) { + (void) fprintf(stderr, gettext("path too long %s/%s\n"), + path, ent->d_name); + ret = ENAMETOOLONG; + continue; + } + + if (asprintf(&fullname, "%s/%s", path, ent->d_name) == -1) { + (void) fprintf(stderr, + gettext("failed to allocate memory\n")); + ret = ENOMEM; + continue; + } + + if (xdev) { + if (stat(fullname, &st) < 0) { + ret = errno; + (void) fprintf(stderr, + gettext("failed to stat %s: %s\n"), + fullname, strerror(errno)); + free(fullname); + continue; + } + if (st.st_dev != dev) { + free(fullname); + continue; + } + } + + if (ent->d_type == DT_REG) { + err = zfs_rewrite_file(fullname, verbose, args); + if (err) + ret = err; + } else { /* DT_DIR */ + fnvlist_add_uint64(dirs, fullname, dev); + } + + free(fullname); + } + + closedir(dir); + return (ret); +} + +static int +zfs_rewrite_path(const char *path, boolean_t verbose, boolean_t recurse, + boolean_t xdev, zfs_rewrite_args_t *args, nvlist_t *dirs) +{ + struct stat st; + int ret = 0; + + if (stat(path, &st) < 0) { + ret = errno; + (void) fprintf(stderr, gettext("failed to stat %s: %s\n"), + path, strerror(errno)); + return (ret); + } + + if (S_ISREG(st.st_mode)) { + ret = zfs_rewrite_file(path, verbose, args); + } else if (S_ISDIR(st.st_mode) && recurse) { + ret = zfs_rewrite_dir(path, verbose, xdev, st.st_dev, args, + dirs); + } + return (ret); +} + +static int +zfs_do_rewrite(int argc, char **argv) +{ + int ret = 0, err, c; + boolean_t recurse = B_FALSE, verbose = B_FALSE, xdev = B_FALSE; + + if (argc < 2) + usage(B_FALSE); + + zfs_rewrite_args_t args; + memset(&args, 0, sizeof (args)); + + while ((c = getopt(argc, argv, "l:o:rvx")) != -1) { + switch (c) { + case 'l': + args.len = strtoll(optarg, NULL, 0); + break; + case 'o': + args.off = strtoll(optarg, NULL, 0); + break; + case 'r': + recurse = B_TRUE; + break; + case 'v': + verbose = B_TRUE; + break; + case 'x': + xdev = B_TRUE; + break; + default: + (void) fprintf(stderr, gettext("invalid option '%c'\n"), + optopt); + usage(B_FALSE); + } + } + + argv += optind; + argc -= optind; + if (argc == 0) { + (void) fprintf(stderr, + gettext("missing file or directory target(s)\n")); + usage(B_FALSE); + } + + nvlist_t *dirs = fnvlist_alloc(); + for (int i = 0; i < argc; i++) { + err = zfs_rewrite_path(argv[i], verbose, recurse, xdev, &args, + dirs); + if (err) + ret = err; + } + nvpair_t *dir; + while ((dir = nvlist_next_nvpair(dirs, NULL)) != NULL) { + err = zfs_rewrite_dir(nvpair_name(dir), verbose, xdev, + fnvpair_value_uint64(dir), &args, dirs); + if (err) + ret = err; + fnvlist_remove_nvpair(dirs, dir); + } + fnvlist_free(dirs); + + return (ret); +} + static int zfs_do_wait(int argc, char **argv) { diff --git a/contrib/debian/openzfs-zfsutils.install b/contrib/debian/openzfs-zfsutils.install index 546745930bff..4573cc77ea74 100644 --- a/contrib/debian/openzfs-zfsutils.install +++ b/contrib/debian/openzfs-zfsutils.install @@ -73,6 +73,7 @@ usr/share/man/man8/zfs-recv.8 usr/share/man/man8/zfs-redact.8 usr/share/man/man8/zfs-release.8 usr/share/man/man8/zfs-rename.8 +usr/share/man/man8/zfs-rewrite.8 usr/share/man/man8/zfs-rollback.8 usr/share/man/man8/zfs-send.8 usr/share/man/man8/zfs-set.8 diff --git a/include/sys/fs/zfs.h b/include/sys/fs/zfs.h index 44d63e8708cb..c8deb5be419e 100644 --- a/include/sys/fs/zfs.h +++ b/include/sys/fs/zfs.h @@ -1620,6 +1620,15 @@ typedef enum zfs_ioc { #endif +typedef struct zfs_rewrite_args { + uint64_t off; + uint64_t len; + uint64_t flags; + uint64_t arg; +} zfs_rewrite_args_t; + +#define ZFS_IOC_REWRITE _IOW(0x83, 3, zfs_rewrite_args_t) + /* * ZFS-specific error codes used for returning descriptive errors * to the userland through zfs ioctls. diff --git a/include/sys/zfs_vnops.h b/include/sys/zfs_vnops.h index 21f0da4fe6b4..08cf0e2a6e48 100644 --- a/include/sys/zfs_vnops.h +++ b/include/sys/zfs_vnops.h @@ -40,6 +40,7 @@ extern int zfs_clone_range(znode_t *, uint64_t *, znode_t *, uint64_t *, uint64_t *, cred_t *); extern int zfs_clone_range_replay(znode_t *, uint64_t, uint64_t, uint64_t, const blkptr_t *, size_t); +extern int zfs_rewrite(znode_t *, uint64_t, uint64_t, uint64_t, uint64_t); extern int zfs_getsecattr(znode_t *, vsecattr_t *, int, cred_t *); extern int zfs_setsecattr(znode_t *, vsecattr_t *, int, cred_t *); diff --git a/man/Makefile.am b/man/Makefile.am index fde704933764..6a7b2d3e46b7 100644 --- a/man/Makefile.am +++ b/man/Makefile.am @@ -50,6 +50,7 @@ dist_man_MANS = \ %D%/man8/zfs-redact.8 \ %D%/man8/zfs-release.8 \ %D%/man8/zfs-rename.8 \ + %D%/man8/zfs-rewrite.8 \ %D%/man8/zfs-rollback.8 \ %D%/man8/zfs-send.8 \ %D%/man8/zfs-set.8 \ diff --git a/man/man8/zfs-rewrite.8 b/man/man8/zfs-rewrite.8 new file mode 100644 index 000000000000..5fb360d907a0 --- /dev/null +++ b/man/man8/zfs-rewrite.8 @@ -0,0 +1,59 @@ +.\" SPDX-License-Identifier: CDDL-1.0 +.\" +.\" CDDL HEADER START +.\" +.\" The contents of this file are subject to the terms of the +.\" Common Development and Distribution License (the "License"). +.\" You may not use this file except in compliance with the License. +.\" +.\" You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE +.\" or https://opensource.org/licenses/CDDL-1.0. +.\" See the License for the specific language governing permissions +.\" and limitations under the License. +.\" +.\" When distributing Covered Code, include this CDDL HEADER in each +.\" file and include the License file at usr/src/OPENSOLARIS.LICENSE. +.\" If applicable, add the following below this CDDL HEADER, with the +.\" fields enclosed by brackets "[]" replaced with your own identifying +.\" information: Portions Copyright [yyyy] [name of copyright owner] +.\" +.\" CDDL HEADER END +.\" +.\" Copyright (c) 2025 iXsystems, Inc. +.\" +.Dd April 30, 2025 +.Dt ZFS-REWRITE 8 +.Os +. +.Sh NAME +.Nm zfs-rewrite +.Nd rewrite specified files without modification +.Sh SYNOPSIS +.Nm zfs +.Cm rewrite +.Oo Fl rvx Ns Oc +.Op Fl l Ar length +.Op Fl o Ar offset +.Ar file Ns | Ns Ar directory Ns … +. +.Sh DESCRIPTION +Rewrite blocks of specified +.Ar file +as is without modification at a new location and possibly with new +properties, such as checksum, compression, dedup, copies, etc, +as if they were atomically read and written back. +.Bl -tag -width "-r" +.It Fl l Ar length +Rewrite at most this number of bytes. +.It Fl o Ar offset +Start at this offset in bytes. +.It Fl r +Recurse into directories. +.It Fl v +Print names of all successfully rewritten files. +.It Fl x +Don't cross file system mount points when recursing. +.El +. +.Sh SEE ALSO +.Xr zfsprops 7 diff --git a/man/man8/zfs.8 b/man/man8/zfs.8 index 5bdeb7f9e455..e16a3a82b672 100644 --- a/man/man8/zfs.8 +++ b/man/man8/zfs.8 @@ -37,7 +37,7 @@ .\" Copyright 2018 Nexenta Systems, Inc. .\" Copyright 2019 Joyent, Inc. .\" -.Dd May 12, 2022 +.Dd April 18, 2025 .Dt ZFS 8 .Os . @@ -299,6 +299,12 @@ Execute ZFS administrative operations programmatically via a Lua script-language channel program. .El . +.Ss Data rewrite +.Bl -tag -width "" +.It Xr zfs-rewrite 8 +Rewrite specified files without modification. +.El +. .Ss Jails .Bl -tag -width "" .It Xr zfs-jail 8 diff --git a/module/os/freebsd/zfs/zfs_vnops_os.c b/module/os/freebsd/zfs/zfs_vnops_os.c index b2080a48c4ad..0fa2003554cc 100644 --- a/module/os/freebsd/zfs/zfs_vnops_os.c +++ b/module/os/freebsd/zfs/zfs_vnops_os.c @@ -305,6 +305,18 @@ zfs_ioctl(vnode_t *vp, ulong_t com, intptr_t data, int flag, cred_t *cred, *(offset_t *)data = off; return (0); } + case ZFS_IOC_REWRITE: { + zfs_rewrite_args_t *args = (zfs_rewrite_args_t *)data; + if ((flag & FWRITE) == 0) + return (SET_ERROR(EBADF)); + error = vn_lock(vp, LK_SHARED); + if (error) + return (error); + error = zfs_rewrite(VTOZ(vp), args->off, args->len, + args->flags, args->arg); + VOP_UNLOCK(vp); + return (error); + } } return (SET_ERROR(ENOTTY)); } diff --git a/module/os/linux/zfs/zpl_file.c b/module/os/linux/zfs/zpl_file.c index 4d10d130ffb0..04b59a5e4a03 100644 --- a/module/os/linux/zfs/zpl_file.c +++ b/module/os/linux/zfs/zpl_file.c @@ -986,6 +986,27 @@ zpl_ioctl_setdosflags(struct file *filp, void __user *arg) return (err); } +static int +zpl_ioctl_rewrite(struct file *filp, void __user *arg) +{ + struct inode *ip = file_inode(filp); + zfs_rewrite_args_t args; + fstrans_cookie_t cookie; + int err; + + if (copy_from_user(&args, arg, sizeof (args))) + return (-EFAULT); + + if (unlikely(!(filp->f_mode & FMODE_WRITE))) + return (-EBADF); + + cookie = spl_fstrans_mark(); + err = -zfs_rewrite(ITOZ(ip), args.off, args.len, args.flags, args.arg); + spl_fstrans_unmark(cookie); + + return (err); +} + static long zpl_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { @@ -1010,6 +1031,8 @@ zpl_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) return (zpl_ioctl_ficlonerange(filp, (void *)arg)); case ZFS_IOC_COMPAT_FIDEDUPERANGE: return (zpl_ioctl_fideduperange(filp, (void *)arg)); + case ZFS_IOC_REWRITE: + return (zpl_ioctl_rewrite(filp, (void *)arg)); default: return (-ENOTTY); } diff --git a/module/zfs/zfs_vnops.c b/module/zfs/zfs_vnops.c index afd9e61313a9..29143a057cc7 100644 --- a/module/zfs/zfs_vnops.c +++ b/module/zfs/zfs_vnops.c @@ -1050,6 +1050,143 @@ zfs_write(znode_t *zp, zfs_uio_t *uio, int ioflag, cred_t *cr) return (0); } +/* + * Rewrite a range of file as-is without modification. + * + * IN: zp - znode of file to be rewritten. + * off - Offset of the range to rewrite. + * len - Length of the range to rewrite. + * flags - Random rewrite parameters. + * arg - flags-specific argument. + * + * RETURN: 0 if success + * error code if failure + */ +int +zfs_rewrite(znode_t *zp, uint64_t off, uint64_t len, uint64_t flags, + uint64_t arg) +{ + int error; + + if (flags != 0 || arg != 0) + return (SET_ERROR(EINVAL)); + + zfsvfs_t *zfsvfs = ZTOZSB(zp); + if ((error = zfs_enter_verify_zp(zfsvfs, zp, FTAG)) != 0) + return (error); + + if (zfs_is_readonly(zfsvfs)) { + zfs_exit(zfsvfs, FTAG); + return (SET_ERROR(EROFS)); + } + + if (off >= zp->z_size) { + zfs_exit(zfsvfs, FTAG); + return (0); + } + if (len == 0 || len > zp->z_size - off) + len = zp->z_size - off; + + /* Flush any mmap()'d data to disk */ + if (zn_has_cached_data(zp, off, off + len - 1)) + zn_flush_cached_data(zp, B_TRUE); + + zfs_locked_range_t *lr; + lr = zfs_rangelock_enter(&zp->z_rangelock, off, len, RL_WRITER); + + const uint64_t uid = KUID_TO_SUID(ZTOUID(zp)); + const uint64_t gid = KGID_TO_SGID(ZTOGID(zp)); + const uint64_t projid = zp->z_projid; + + dmu_buf_impl_t *db = (dmu_buf_impl_t *)sa_get_db(zp->z_sa_hdl); + DB_DNODE_ENTER(db); + dnode_t *dn = DB_DNODE(db); + + uint64_t n, noff = off, nr = 0, nw = 0; + while (len > 0) { + /* + * Rewrite only actual data, skipping any holes. This might + * be inaccurate for dirty files, but we don't really care. + */ + if (noff == off) { + /* Find next data in the file. */ + error = dnode_next_offset(dn, 0, &noff, 1, 1, 0); + if (error || noff >= off + len) { + if (error == ESRCH) /* No more data. */ + error = 0; + break; + } + ASSERT3U(noff, >=, off); + len -= noff - off; + off = noff; + + /* Find where the data end. */ + error = dnode_next_offset(dn, DNODE_FIND_HOLE, &noff, + 1, 1, 0); + if (error != 0) + noff = off + len; + } + ASSERT3U(noff, >, off); + + if (zfs_id_overblockquota(zfsvfs, DMU_USERUSED_OBJECT, uid) || + zfs_id_overblockquota(zfsvfs, DMU_GROUPUSED_OBJECT, gid) || + (projid != ZFS_DEFAULT_PROJID && + zfs_id_overblockquota(zfsvfs, DMU_PROJECTUSED_OBJECT, + projid))) { + error = SET_ERROR(EDQUOT); + break; + } + + n = MIN(MIN(len, noff - off), + DMU_MAX_ACCESS / 2 - P2PHASE(off, zp->z_blksz)); + + dmu_tx_t *tx = dmu_tx_create(zfsvfs->z_os); + dmu_tx_hold_write_by_dnode(tx, dn, off, n); + error = dmu_tx_assign(tx, DMU_TX_WAIT); + if (error) { + dmu_tx_abort(tx); + break; + } + + /* Mark all dbufs within range as dirty to trigger rewrite. */ + dmu_buf_t **dbp; + int numbufs; + error = dmu_buf_hold_array_by_dnode(dn, off, n, TRUE, FTAG, + &numbufs, &dbp, DMU_READ_PREFETCH); + if (error) { + dmu_tx_abort(tx); + break; + } + for (int i = 0; i < numbufs; i++) { + nr += dbp[i]->db_size; + if (dmu_buf_is_dirty(dbp[i], tx)) + continue; + nw += dbp[i]->db_size; + dmu_buf_will_dirty(dbp[i], tx); + } + dmu_buf_rele_array(dbp, numbufs, FTAG); + + dmu_tx_commit(tx); + + len -= n; + off += n; + + if (issig()) { + error = SET_ERROR(EINTR); + break; + } + } + + DB_DNODE_EXIT(db); + + dataset_kstats_update_read_kstats(&zfsvfs->z_kstat, nr); + dataset_kstats_update_write_kstats(&zfsvfs->z_kstat, nw); + + zfs_rangelock_exit(lr); + zfs_exit(zfsvfs, FTAG); + return (error); +} + int zfs_getsecattr(znode_t *zp, vsecattr_t *vsecp, int flag, cred_t *cr) { diff --git a/tests/runfiles/common.run b/tests/runfiles/common.run index 9373b39a184a..83be43e7cacc 100644 --- a/tests/runfiles/common.run +++ b/tests/runfiles/common.run @@ -306,6 +306,10 @@ tags = ['functional', 'cli_root', 'zfs_rename'] tests = ['zfs_reservation_001_pos', 'zfs_reservation_002_pos'] tags = ['functional', 'cli_root', 'zfs_reservation'] +[tests/functional/cli_root/zfs_rewrite] +tests = ['zfs_rewrite'] +tags = ['functional', 'cli_root', 'zfs_rewrite'] + [tests/functional/cli_root/zfs_rollback] tests = ['zfs_rollback_001_pos', 'zfs_rollback_002_pos', 'zfs_rollback_003_neg', 'zfs_rollback_004_neg'] diff --git a/tests/runfiles/sanity.run b/tests/runfiles/sanity.run index ddd2d431a5b6..75c7897a13db 100644 --- a/tests/runfiles/sanity.run +++ b/tests/runfiles/sanity.run @@ -194,6 +194,10 @@ tags = ['functional', 'cli_root', 'zfs_rename'] tests = ['zfs_reservation_001_pos', 'zfs_reservation_002_pos'] tags = ['functional', 'cli_root', 'zfs_reservation'] +[tests/functional/cli_root/zfs_rewrite] +tests = ['zfs_rewrite'] +tags = ['functional', 'cli_root', 'zfs_rewrite'] + [tests/functional/cli_root/zfs_rollback] tests = ['zfs_rollback_003_neg', 'zfs_rollback_004_neg'] tags = ['functional', 'cli_root', 'zfs_rollback'] diff --git a/tests/zfs-tests/tests/Makefile.am b/tests/zfs-tests/tests/Makefile.am index 6a5d11761874..4536369406f8 100644 --- a/tests/zfs-tests/tests/Makefile.am +++ b/tests/zfs-tests/tests/Makefile.am @@ -862,6 +862,9 @@ nobase_dist_datadir_zfs_tests_tests_SCRIPTS += \ functional/cli_root/zfs_reservation/setup.ksh \ functional/cli_root/zfs_reservation/zfs_reservation_001_pos.ksh \ functional/cli_root/zfs_reservation/zfs_reservation_002_pos.ksh \ + functional/cli_root/zfs_rewrite/cleanup.ksh \ + functional/cli_root/zfs_rewrite/setup.ksh \ + functional/cli_root/zfs_rewrite/zfs_rewrite.ksh \ functional/cli_root/zfs_rollback/cleanup.ksh \ functional/cli_root/zfs_rollback/setup.ksh \ functional/cli_root/zfs_rollback/zfs_rollback_001_pos.ksh \ diff --git a/tests/zfs-tests/tests/functional/cli_root/zfs_rewrite/cleanup.ksh b/tests/zfs-tests/tests/functional/cli_root/zfs_rewrite/cleanup.ksh new file mode 100755 index 000000000000..5e73dd34936e --- /dev/null +++ b/tests/zfs-tests/tests/functional/cli_root/zfs_rewrite/cleanup.ksh @@ -0,0 +1,26 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# CDDL HEADER START +# +# The contents of this file are subject to the terms of the +# Common Development and Distribution License (the "License"). +# You may not use this file except in compliance with the License. +# +# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE +# or https://opensource.org/licenses/CDDL-1.0. +# See the License for the specific language governing permissions +# and limitations under the License. +# +# When distributing Covered Code, include this CDDL HEADER in each +# file and include the License file at usr/src/OPENSOLARIS.LICENSE. +# If applicable, add the following below this CDDL HEADER, with the +# fields enclosed by brackets "[]" replaced with your own identifying +# information: Portions Copyright [yyyy] [name of copyright owner] +# +# CDDL HEADER END +# + +. $STF_SUITE/include/libtest.shlib + +default_cleanup diff --git a/tests/zfs-tests/tests/functional/cli_root/zfs_rewrite/setup.ksh b/tests/zfs-tests/tests/functional/cli_root/zfs_rewrite/setup.ksh new file mode 100755 index 000000000000..dddfdf8a4679 --- /dev/null +++ b/tests/zfs-tests/tests/functional/cli_root/zfs_rewrite/setup.ksh @@ -0,0 +1,28 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# CDDL HEADER START +# +# The contents of this file are subject to the terms of the +# Common Development and Distribution License (the "License"). +# You may not use this file except in compliance with the License. +# +# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE +# or https://opensource.org/licenses/CDDL-1.0. +# See the License for the specific language governing permissions +# and limitations under the License. +# +# When distributing Covered Code, include this CDDL HEADER in each +# file and include the License file at usr/src/OPENSOLARIS.LICENSE. +# If applicable, add the following below this CDDL HEADER, with the +# fields enclosed by brackets "[]" replaced with your own identifying +# information: Portions Copyright [yyyy] [name of copyright owner] +# +# CDDL HEADER END +# + +. $STF_SUITE/include/libtest.shlib + +DISK=${DISKS%% *} + +default_setup $DISK diff --git a/tests/zfs-tests/tests/functional/cli_root/zfs_rewrite/zfs_rewrite.ksh b/tests/zfs-tests/tests/functional/cli_root/zfs_rewrite/zfs_rewrite.ksh new file mode 100755 index 000000000000..d1c0b3c64c27 --- /dev/null +++ b/tests/zfs-tests/tests/functional/cli_root/zfs_rewrite/zfs_rewrite.ksh @@ -0,0 +1,104 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# CDDL HEADER START +# +# The contents of this file are subject to the terms of the +# Common Development and Distribution License (the "License"). +# You may not use this file except in compliance with the License. +# +# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE +# or https://opensource.org/licenses/CDDL-1.0. +# See the License for the specific language governing permissions +# and limitations under the License. +# +# When distributing Covered Code, include this CDDL HEADER in each +# file and include the License file at usr/src/OPENSOLARIS.LICENSE. +# If applicable, add the following below this CDDL HEADER, with the +# fields enclosed by brackets "[]" replaced with your own identifying +# information: Portions Copyright [yyyy] [name of copyright owner] +# +# CDDL HEADER END +# + +# +# Copyright (c) 2025, iXsystems, Inc. +# + +# DESCRIPTION: +# Verify zfs rewrite rewrites specified files blocks. +# +# STRATEGY: +# 1. Create two files, one of which is in a directory. +# 2. Save the checksums and block pointers. +# 3. Rewrite part of the files. +# 4. Verify checksums are the same. +# 5. Verify block pointers of the rewritten part have changed. +# 6. Rewrite all the files. +# 7. Verify checksums are the same. +# 8. Verify all block pointers have changed. + +. $STF_SUITE/include/libtest.shlib + +typeset tmp=$(mktemp) +typeset bps=$(mktemp) +typeset bps1=$(mktemp) +typeset bps2=$(mktemp) + +function cleanup +{ + rm -rf $tmp $bps $bps1 $bps2 $TESTDIR/* +} + +log_assert "zfs rewrite rewrites specified files blocks" + +log_onexit cleanup + +log_must zfs set recordsize=128k $TESTPOOL/$TESTFS + +log_must mkdir $TESTDIR/dir +log_must dd if=/dev/urandom of=$TESTDIR/file1 bs=128k count=8 +log_must dd if=$TESTDIR/file1 of=$TESTDIR/dir/file2 bs=128k +log_must sync_pool $TESTPOOL +typeset orig_hash1=$(xxh128digest $TESTDIR/file1) +typeset orig_hash2=$(xxh128digest $TESTDIR/dir/file2) + +log_must [ "$orig_hash1" = "$orig_hash2" ] +log_must eval "zdb -Ovv $TESTPOOL/$TESTFS file1 > $tmp" +log_must eval "awk '/ L0 / { print l++ \" \" \$3 }' < $tmp > $bps1" +log_must eval "zdb -Ovv $TESTPOOL/$TESTFS dir/file2 > $tmp" +log_must eval "awk '/ L0 / { print l++ \" \" \$3 }' < $tmp > $bps2" + +log_must zfs rewrite -o 327680 -l 262144 -r -x $TESTDIR/file1 $TESTDIR/dir/file2 +log_must sync_pool $TESTPOOL +typeset new_hash1=$(xxh128digest $TESTDIR/file1) +typeset new_hash2=$(xxh128digest $TESTDIR/dir/file2) +log_must [ "$orig_hash1" = "$new_hash1" ] +log_must [ "$orig_hash2" = "$new_hash2" ] + +log_must eval "zdb -Ovv $TESTPOOL/$TESTFS file1 > $tmp" +log_must eval "awk '/ L0 / { print l++ \" \" \$3 }' < $tmp > $bps" +typeset same=$(echo $(sort -n $bps $bps1 | uniq -d | cut -f1 -d' ')) +log_must [ "$same" = "0 1 5 6 7" ] +log_must eval "zdb -Ovv $TESTPOOL/$TESTFS dir/file2 > $tmp" +log_must eval "awk '/ L0 / { print l++ \" \" \$3 }' < $tmp > $bps" +typeset same=$(echo $(sort -n $bps $bps2 | uniq -d | cut -f1 -d' ')) +log_must [ "$same" = "0 1 5 6 7" ] + +log_must zfs rewrite -r $TESTDIR/file1 $TESTDIR/dir/file2 +log_must sync_pool $TESTPOOL +typeset new_hash1=$(xxh128digest $TESTDIR/file1) +typeset new_hash2=$(xxh128digest $TESTDIR/dir/file2) +log_must [ "$orig_hash1" = "$new_hash1" ] +log_must [ "$orig_hash2" = "$new_hash2" ] + +log_must eval "zdb -Ovv $TESTPOOL/$TESTFS file1 > $tmp" +log_must eval "awk '/ L0 / { print l++ \" \" \$3 }' < $tmp > $bps" +typeset same=$(echo $(sort -n $bps $bps1 | uniq -d | cut -f1 -d' ')) +log_must [ -z "$same" ] +log_must eval "zdb -Ovv $TESTPOOL/$TESTFS dir/file2 > $tmp" +log_must eval "awk '/ L0 / { print l++ \" \" \$3 }' < $tmp > $bps" +typeset same=$(echo $(sort -n $bps $bps2 | uniq -d | cut -f1 -d' ')) +log_must [ -z "$same" ] + +log_pass