From 6e6f97fc7d676fbb1632845212f8f806204ba5ab 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 | 76 +++++++ 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, 636 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 d706ef5fc38c..665e183485f0 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 (lstat(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 (lstat(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..423d6d439e28 --- /dev/null +++ b/man/man8/zfs-rewrite.8 @@ -0,0 +1,76 @@ +.\" 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 May 6, 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 NOTES +Rewrite of cloned blocks and blocks that are part of any snapshots, +same as some property changes may increase pool space usage. +Holes that were never written or were previously zero-compressed are +not rewritten and will remain holes even if compression is disabled. +.Pp +Rewritten blocks will be seen as modified in next snapshot and as such +included into the incremental +.Nm zfs Cm send +stream. +.Pp +If a +.Fl l +or +.Fl o +value request a rewrite to regions past the end of the file, then those +regions are silently ignored, and no error is reported. +. +.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 d7f3c75c7948..9ea511dec0eb 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 6362a2606260..9483eac40efb 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 4c102b3aa1b8..acff5e57db93 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