diff --git a/cmd/zfs/zfs_iter.c b/cmd/zfs/zfs_iter.c index bbb6cc55dbd7..efae4eb5f339 100644 --- a/cmd/zfs/zfs_iter.c +++ b/cmd/zfs/zfs_iter.c @@ -148,7 +148,15 @@ zfs_callback(zfs_handle_t *zhp, void *data) if (((zfs_get_type(zhp) & (ZFS_TYPE_SNAPSHOT | ZFS_TYPE_BOOKMARK)) == 0) && include_snaps) { - (void) zfs_iter_snapshots_v2(zhp, cb->cb_flags, + int snapshot_flags = cb->cb_flags; + + /* + * Default sorting and tie breaks require the + * creation TXG. + */ + if (snapshot_flags & ZFS_ITER_BATCHED) + snapshot_flags |= ZFS_ITER_BATCHED_CREATETXG; + (void) zfs_iter_snapshots_v2(zhp, snapshot_flags, zfs_callback, data, 0, 0); } @@ -265,6 +273,135 @@ zfs_list_only_by_fast(const zprop_list_t *p) return (B_TRUE); } +static boolean_t +zfs_list_batch_inapplicable_prop(int prop) +{ + return (prop >= ZFS_PROP_TYPE && prop < ZFS_NUM_PROPS && + !zfs_prop_valid_for_type(prop, + ZFS_TYPE_SNAPSHOT | ZFS_TYPE_BOOKMARK, B_FALSE)); +} + +/* + * Select the projected snapshot iterator only when it can populate every + * displayed and sorted property. Optional flags keep unrequested values out + * of the kernel calculation and returned nvlist. + */ +int +zfs_list_batch_flags(const zprop_list_t *p, const zfs_sort_column_t *sc) +{ + int flags = ZFS_ITER_BATCHED; + + if (p == NULL) + return (0); + + for (; p != NULL; p = p->pl_next) { + if (p->pl_all) + return (0); + + switch (p->pl_prop) { + case ZFS_PROP_NAME: + case ZFS_PROP_TYPE: + break; + case ZFS_PROP_USED: + flags |= ZFS_ITER_BATCHED_USED; + break; + case ZFS_PROP_REFERENCED: + flags |= ZFS_ITER_BATCHED_REFERENCED; + break; + case ZFS_PROP_LOGICALREFERENCED: + flags |= ZFS_ITER_BATCHED_LOGICALREFERENCED; + break; + case ZFS_PROP_DEFER_DESTROY: + flags |= ZFS_ITER_BATCHED_DEFER_DESTROY; + break; + case ZFS_PROP_WRITTEN: + flags |= ZFS_ITER_BATCHED_WRITTEN; + break; + case ZFS_PROP_OBJSETID: + flags |= ZFS_ITER_BATCHED_OBJSETID; + break; + case ZFS_PROP_GUID: + flags |= ZFS_ITER_BATCHED_GUID; + break; + case ZFS_PROP_CREATETXG: + flags |= ZFS_ITER_BATCHED_CREATETXG; + break; + case ZFS_PROP_CREATION: + flags |= ZFS_ITER_BATCHED_CREATION; + break; + case ZFS_PROP_USERREFS: + flags |= ZFS_ITER_BATCHED_USERREFS; + break; + case ZFS_PROP_NUMCLONES: + flags |= ZFS_ITER_BATCHED_NUMCLONES; + break; + case ZFS_PROP_INCONSISTENT: + flags |= ZFS_ITER_BATCHED_INCONSISTENT; + break; + case ZFS_PROP_REDACTED: + flags |= ZFS_ITER_BATCHED_REDACTED; + break; + default: + if (!zfs_list_batch_inapplicable_prop(p->pl_prop)) + return (0); + break; + } + } + + for (; sc != NULL; sc = sc->sc_next) { + switch (sc->sc_prop) { + case ZFS_PROP_NAME: + case ZFS_PROP_TYPE: + break; + case ZFS_PROP_USED: + flags |= ZFS_ITER_BATCHED_USED; + break; + case ZFS_PROP_REFERENCED: + flags |= ZFS_ITER_BATCHED_REFERENCED; + break; + case ZFS_PROP_LOGICALREFERENCED: + flags |= ZFS_ITER_BATCHED_LOGICALREFERENCED; + break; + case ZFS_PROP_DEFER_DESTROY: + flags |= ZFS_ITER_BATCHED_DEFER_DESTROY; + break; + case ZFS_PROP_WRITTEN: + flags |= ZFS_ITER_BATCHED_WRITTEN; + break; + case ZFS_PROP_OBJSETID: + flags |= ZFS_ITER_BATCHED_OBJSETID; + break; + case ZFS_PROP_GUID: + flags |= ZFS_ITER_BATCHED_GUID; + break; + case ZFS_PROP_CREATETXG: + flags |= ZFS_ITER_BATCHED_CREATETXG; + break; + case ZFS_PROP_CREATION: + flags |= ZFS_ITER_BATCHED_CREATION; + break; + case ZFS_PROP_USERREFS: + flags |= ZFS_ITER_BATCHED_USERREFS; + break; + case ZFS_PROP_NUMCLONES: + flags |= ZFS_ITER_BATCHED_NUMCLONES; + break; + case ZFS_PROP_INCONSISTENT: + flags |= ZFS_ITER_BATCHED_INCONSISTENT; + break; + case ZFS_PROP_REDACTED: + flags |= ZFS_ITER_BATCHED_REDACTED; + break; + default: + if (!zfs_list_batch_inapplicable_prop(sc->sc_prop)) + return (0); + break; + } + } + + return (flags); +} + static int zfs_compare(const void *larg, const void *rarg) { diff --git a/cmd/zfs/zfs_iter.h b/cmd/zfs/zfs_iter.h index 256d64d0030f..b696f23597e2 100644 --- a/cmd/zfs/zfs_iter.h +++ b/cmd/zfs/zfs_iter.h @@ -47,6 +47,7 @@ int zfs_add_sort_column(zfs_sort_column_t **, const char *, boolean_t); void zfs_free_sort_columns(zfs_sort_column_t *); boolean_t zfs_sort_only_by_fast(const zfs_sort_column_t *); boolean_t zfs_list_only_by_fast(const zprop_list_t *); +int zfs_list_batch_flags(const zprop_list_t *, const zfs_sort_column_t *); #ifdef __cplusplus } diff --git a/cmd/zfs/zfs_main.c b/cmd/zfs/zfs_main.c index 0cd2fcae65f1..ee0189fd3a78 100644 --- a/cmd/zfs/zfs_main.c +++ b/cmd/zfs/zfs_main.c @@ -3894,6 +3894,17 @@ list_callback(zfs_handle_t *zhp, void *data) return (0); } +static boolean_t +zfs_list_has_prop(const zprop_list_t *pl, zfs_prop_t prop) +{ + for (; pl != NULL; pl = pl->pl_next) { + if (pl->pl_prop == prop) + return (B_TRUE); + } + + return (B_FALSE); +} + static int zfs_do_list(int argc, char **argv) { @@ -4051,10 +4062,25 @@ found3:; cb.cb_first = B_TRUE; /* - * If we are only going to list and sort by properties that are "fast" - * then we can use "simple" mode and avoid populating the properties - * nvlist. + * Use projected batches when they contain every displayed and sorted + * property. Keep simple mode available for fallback to a kernel that + * predates the batch ioctl. */ + int batch_flags = zfs_list_batch_flags(cb.cb_proplist, sortcol); + /* + * Although AVAILABLE prints "-" for snapshots, its full-stat value is + * used with USED to color the field. Projected handles omit it. + */ + if (batch_flags != 0 && !cb.cb_json && use_color() && + (types & ZFS_TYPE_SNAPSHOT) != 0 && + zfs_list_has_prop(cb.cb_proplist, ZFS_PROP_USED) && + zfs_list_has_prop(cb.cb_proplist, ZFS_PROP_AVAILABLE)) + batch_flags = 0; + if (batch_flags != 0) { + if (cb.cb_json) + batch_flags |= ZFS_ITER_BATCHED_CREATETXG; + flags |= batch_flags; + } if (zfs_list_only_by_fast(cb.cb_proplist) && zfs_sort_only_by_fast(sortcol)) flags |= ZFS_ITER_SIMPLE; diff --git a/include/libzfs.h b/include/libzfs.h index 77506ecd7d72..5f13a4cbc4f0 100644 --- a/include/libzfs.h +++ b/include/libzfs.h @@ -729,6 +729,21 @@ _LIBZFS_H int zprop_collect_property(const char *, zprop_get_cbdata_t *, #define ZFS_ITER_RECVD_PROPS (1 << 4) #define ZFS_ITER_LITERAL_PROPS (1 << 5) #define ZFS_ITER_SIMPLE (1 << 6) +/* Projected snapshot and bookmark fields requested by zfs list. */ +#define ZFS_ITER_BATCHED (1 << 7) +#define ZFS_ITER_BATCHED_CREATION (1 << 8) +#define ZFS_ITER_BATCHED_USERREFS (1 << 9) +#define ZFS_ITER_BATCHED_GUID (1 << 10) +#define ZFS_ITER_BATCHED_CREATETXG (1 << 11) +#define ZFS_ITER_BATCHED_NUMCLONES (1 << 12) +#define ZFS_ITER_BATCHED_INCONSISTENT (1 << 13) +#define ZFS_ITER_BATCHED_REDACTED (1 << 14) +#define ZFS_ITER_BATCHED_USED (1 << 15) +#define ZFS_ITER_BATCHED_REFERENCED (1 << 16) +#define ZFS_ITER_BATCHED_LOGICALREFERENCED (1 << 17) +#define ZFS_ITER_BATCHED_DEFER_DESTROY (1 << 18) +#define ZFS_ITER_BATCHED_OBJSETID (1 << 19) +#define ZFS_ITER_BATCHED_WRITTEN (1 << 20) typedef int (*zfs_iter_f)(zfs_handle_t *, void *); _LIBZFS_H int zfs_iter_root(libzfs_handle_t *, zfs_iter_f, void *); diff --git a/include/os/freebsd/spl/sys/mod.h b/include/os/freebsd/spl/sys/mod.h index c6425e88879f..23e4eb5e2aed 100644 --- a/include/os/freebsd/spl/sys/mod.h +++ b/include/os/freebsd/spl/sys/mod.h @@ -98,6 +98,12 @@ #define param_set_max_auto_ashift_args(var) \ CTLTYPE_UINT, NULL, 0, param_set_max_auto_ashift, "IU" +#define param_set_snapshot_list_batch_size_args(var) \ + CTLTYPE_UINT, NULL, 0, param_set_snapshot_list_batch_size, "IU" + +#define param_set_snapshot_list_batch_time_us_args(var) \ + CTLTYPE_UINT, NULL, 0, param_set_snapshot_list_batch_time_us, "IU" + #define param_set_raidz_impl_args(var) \ CTLTYPE_STRING, NULL, 0, param_set_raidz_impl, "A" diff --git a/include/sys/dsl_dataset.h b/include/sys/dsl_dataset.h index bde559034b26..7a705dd882c0 100644 --- a/include/sys/dsl_dataset.h +++ b/include/sys/dsl_dataset.h @@ -432,6 +432,26 @@ uint64_t dsl_get_inconsistent(dsl_dataset_t *ds); uint64_t dsl_get_redacted(dsl_dataset_t *ds); uint64_t dsl_get_available(dsl_dataset_t *ds); int dsl_get_written(dsl_dataset_t *ds, uint64_t *written); + +typedef struct dsl_dataset_snapshot_stats { + uint64_t dss_creation_txg; + uint64_t dss_creation_time; + uint64_t dss_guid; + uint64_t dss_userrefs; + uint64_t dss_num_clones; + uint64_t dss_used; + uint64_t dss_referenced; + uint64_t dss_logicalreferenced; + uint64_t dss_written; + uint8_t dss_inconsistent; + uint8_t dss_redacted; + uint8_t dss_defer_destroy; + uint8_t dss_written_valid; +} dsl_dataset_snapshot_stats_t; + +int dsl_dataset_snapshot_stats(struct dsl_pool *dp, uint64_t dsobj, + boolean_t want_userrefs, boolean_t want_redacted, boolean_t want_written, + uint64_t min_txg, uint64_t max_txg, dsl_dataset_snapshot_stats_t *stats); int dsl_get_prev_snap(dsl_dataset_t *ds, char *snap); void dsl_get_redact_snaps(dsl_dataset_t *ds, nvlist_t *propval); int dsl_get_mountpoint(dsl_dataset_t *ds, const char *dsname, char *value, diff --git a/include/sys/fs/zfs.h b/include/sys/fs/zfs.h index 02d30985d1fe..dab7175d23ba 100644 --- a/include/sys/fs/zfs.h +++ b/include/sys/fs/zfs.h @@ -1608,6 +1608,28 @@ typedef enum { */ #define SNAP_ITER_MIN_TXG "snap_iter_min_txg" #define SNAP_ITER_MAX_TXG "snap_iter_max_txg" +#define SNAP_ITER_BATCH_CURSOR "snap_iter_batch_cursor" +#define SNAP_ITER_BATCH_MAX_RESULTS "snap_iter_batch_max_results" +#define SNAP_ITER_BATCH_PROPS "snap_iter_batch_props" +#define SNAP_ITER_BATCH_NAMES "snap_iter_batch_names" +#define SNAP_ITER_BATCH_CREATETXGS "snap_iter_batch_createtxgs" +#define SNAP_ITER_BATCH_GUIDS "snap_iter_batch_guids" +#define SNAP_ITER_BATCH_CREATIONS "snap_iter_batch_creations" +#define SNAP_ITER_BATCH_USERREF_COUNTS "snap_iter_batch_userref_counts" +#define SNAP_ITER_BATCH_NUMCLONES "snap_iter_batch_numclones" +#define SNAP_ITER_BATCH_INCONSISTENT "snap_iter_batch_inconsistent" +#define SNAP_ITER_BATCH_REDACTED "snap_iter_batch_redacted" +#define SNAP_ITER_BATCH_USED "snap_iter_batch_used" +#define SNAP_ITER_BATCH_REFERENCED "snap_iter_batch_referenced" +#define SNAP_ITER_BATCH_LOGICALREFERENCED \ + "snap_iter_batch_logicalreferenced" +#define SNAP_ITER_BATCH_DEFER_DESTROY "snap_iter_batch_defer_destroy" +#define SNAP_ITER_BATCH_WRITTENS "snap_iter_batch_writtens" +#define SNAP_ITER_BATCH_WRITTEN_VALID "snap_iter_batch_written_valid" +#define SNAP_ITER_BATCH_OBJSETIDS "snap_iter_batch_objsetids" +#define SNAP_ITER_BATCH_DMU_TYPE "snap_iter_batch_dmu_type" +#define SNAP_ITER_BATCH_DDS_FLAGS "snap_iter_batch_dds_flags" +#define SNAP_ITER_BATCH_EOF "snap_iter_batch_eof" /* * /dev/zfs ioctl numbers. @@ -1616,7 +1638,7 @@ typedef enum { */ typedef enum zfs_ioc { /* - * Core features - 89/128 numbers reserved. + * Core features - 92/128 numbers reserved. */ #ifdef __FreeBSD__ ZFS_IOC_FIRST = 0, @@ -1715,6 +1737,7 @@ typedef enum zfs_ioc { ZFS_IOC_POOL_PREFETCH, /* 0x5a58 */ ZFS_IOC_DDT_PRUNE, /* 0x5a59 */ ZFS_IOC_POOL_CONDENSE, /* 0x5a5a */ + ZFS_IOC_SNAPSHOT_LIST_BATCH, /* 0x5a5b */ /* * Per-platform (Optional) - 8/128 numbers reserved. diff --git a/include/sys/zfs_ioctl_impl.h b/include/sys/zfs_ioctl_impl.h index 74db8a169771..8025ff557f6b 100644 --- a/include/sys/zfs_ioctl_impl.h +++ b/include/sys/zfs_ioctl_impl.h @@ -27,6 +27,14 @@ extern kmutex_t zfsdev_state_lock; extern uint64_t zfs_max_nvlist_src_size; +#define ZFS_SNAPSHOT_LIST_BATCH_SIZE_DEFAULT 1024 +#define ZFS_SNAPSHOT_LIST_BATCH_SIZE_MAX 4096 +#define ZFS_SNAPSHOT_LIST_BATCH_TIME_US_DEFAULT 10000 +#define ZFS_SNAPSHOT_LIST_BATCH_TIME_US_MAX 100000 + +extern uint_t zfs_snapshot_list_batch_size; +extern uint_t zfs_snapshot_list_batch_time_us; + typedef int zfs_ioc_legacy_func_t(zfs_cmd_t *); typedef int zfs_ioc_func_t(const char *, nvlist_t *, nvlist_t *); typedef int zfs_secpolicy_func_t(zfs_cmd_t *, nvlist_t *, cred_t *); @@ -82,6 +90,8 @@ void zfs_ioctl_register(const char *, zfs_ioc_t, zfs_ioc_func_t *, boolean_t, boolean_t, const zfs_ioc_key_t *, size_t); uint64_t zfs_max_nvlist_src_size_os(void); +int param_set_snapshot_list_batch_size(ZFS_MODULE_PARAM_ARGS); +int param_set_snapshot_list_batch_time_us(ZFS_MODULE_PARAM_ARGS); void zfs_ioctl_update_mount_cache(const char *dsname); void zfs_ioctl_init_os(void); diff --git a/lib/libzfs/libzfs.abi b/lib/libzfs/libzfs.abi index 2b597435ecea..8b67a141d1d2 100644 --- a/lib/libzfs/libzfs.abi +++ b/lib/libzfs/libzfs.abi @@ -6616,6 +6616,7 @@ + diff --git a/lib/libzfs/libzfs_dataset.c b/lib/libzfs/libzfs_dataset.c index ea316010629b..137efae7e8b5 100644 --- a/lib/libzfs/libzfs_dataset.c +++ b/lib/libzfs/libzfs_dataset.c @@ -403,6 +403,10 @@ put_stats_zhdl(zfs_handle_t *zhp, zfs_cmd_t *zc) zhp->zfs_props = allprops; zhp->zfs_user_props = userprops; + zhp->zfs_projected_creation = 0; + zhp->zfs_projected_userrefs = 0; + zhp->zfs_projected_props = 0; + zhp->zfs_projected_props_materialized = B_FALSE; return (0); } @@ -604,6 +608,11 @@ zfs_handle_dup(zfs_handle_t *zhp_orig) zhp_orig->zfs_mntopts); } zhp->zfs_props_table = zhp_orig->zfs_props_table; + zhp->zfs_projected_creation = zhp_orig->zfs_projected_creation; + zhp->zfs_projected_userrefs = zhp_orig->zfs_projected_userrefs; + zhp->zfs_projected_props = zhp_orig->zfs_projected_props; + zhp->zfs_projected_props_materialized = + zhp_orig->zfs_projected_props_materialized; return (zhp); } @@ -1865,6 +1874,13 @@ getprop_uint64(zfs_handle_t *zhp, zfs_prop_t prop, const char **source) uint64_t value; *source = NULL; + if (prop == ZFS_PROP_CREATION && + (zhp->zfs_projected_props & ZFS_PROJECTED_CREATION) != 0) + return (zhp->zfs_projected_creation); + if (prop == ZFS_PROP_USERREFS && + (zhp->zfs_projected_props & ZFS_PROJECTED_USERREFS) != 0) + return (zhp->zfs_projected_userrefs); + if (nvlist_lookup_nvlist(zhp->zfs_props, zfs_prop_to_name(prop), &nv) == 0) { value = fnvlist_lookup_uint64(nv, ZPROP_VALUE); @@ -1934,8 +1950,6 @@ static int get_numeric_property(zfs_handle_t *zhp, zfs_prop_t prop, zprop_source_t *src, const char **source, uint64_t *val) { - zfs_cmd_t zc = {"\0"}; - nvlist_t *zplprops = NULL; struct mnttab mnt; const char *mntopt_on = NULL; const char *mntopt_off = NULL; @@ -2083,7 +2097,10 @@ get_numeric_property(zfs_handle_t *zhp, zfs_prop_t prop, zprop_source_t *src, case ZFS_PROP_DEFAULTPROJECTQUOTA: case ZFS_PROP_DEFAULTUSEROBJQUOTA: case ZFS_PROP_DEFAULTGROUPOBJQUOTA: - case ZFS_PROP_DEFAULTPROJECTOBJQUOTA: + case ZFS_PROP_DEFAULTPROJECTOBJQUOTA: { + zfs_cmd_t zc = {"\0"}; + nvlist_t *zplprops = NULL; + zcmd_alloc_dst_nvlist(zhp->zfs_hdl, &zc, 0); (void) strlcpy(zc.zc_name, zhp->zfs_name, sizeof (zc.zc_name)); @@ -2103,6 +2120,7 @@ get_numeric_property(zfs_handle_t *zhp, zfs_prop_t prop, zprop_source_t *src, nvlist_free(zplprops); zcmd_free_nvlists(&zc); break; + } case ZFS_PROP_INCONSISTENT: *val = zhp->zfs_dmustats.dds_inconsistent; @@ -4510,10 +4528,60 @@ zfs_rename(zfs_handle_t *zhp, const char *target, renameflags_t flags) return (ret); } +static int +zfs_projected_prop_add(nvlist_t *props, zfs_prop_t prop, uint64_t value) +{ + nvlist_t *propval; + int error; + + if ((error = nvlist_alloc(&propval, NV_UNIQUE_NAME, 0)) != 0) + return (error); + if ((error = nvlist_add_uint64(propval, ZPROP_VALUE, value)) == 0) { + error = nvlist_add_nvlist(props, zfs_prop_to_name(prop), + propval); + } + nvlist_free(propval); + return (error); +} + +/* + * Return the handle's property nvlist. Projected handles retain hot numeric + * values directly and materialize their traditional nested entries only for + * callers of this interface. This preserves the existing nvlist shape while + * keeping normal property access allocation-free. + */ nvlist_t * zfs_get_all_props(zfs_handle_t *zhp) { + int error; + + if (!zhp->zfs_projected_props_materialized && + zhp->zfs_projected_props != 0) { + if ((zhp->zfs_projected_props & ZFS_PROJECTED_CREATION) != 0) { + error = zfs_projected_prop_add(zhp->zfs_props, + ZFS_PROP_CREATION, zhp->zfs_projected_creation); + if (error != 0) + goto nomem; + } + if ((zhp->zfs_projected_props & ZFS_PROJECTED_USERREFS) != 0) { + error = zfs_projected_prop_add(zhp->zfs_props, + ZFS_PROP_USERREFS, zhp->zfs_projected_userrefs); + if (error != 0) + goto rollback; + } + zhp->zfs_projected_props_materialized = B_TRUE; + } return (zhp->zfs_props); + +rollback: + if ((zhp->zfs_projected_props & ZFS_PROJECTED_CREATION) != 0) { + fnvlist_remove(zhp->zfs_props, + zfs_prop_to_name(ZFS_PROP_CREATION)); + } +nomem: + errno = error; + (void) no_memory(zhp->zfs_hdl); + return (NULL); } nvlist_t * @@ -4647,6 +4715,10 @@ zfs_prune_proplist(zfs_handle_t *zhp, uint8_t *props) * properties. */ zhp->zfs_props_table = props; + if (props[ZFS_PROP_CREATION] == B_FALSE) + zhp->zfs_projected_props &= ~ZFS_PROJECTED_CREATION; + if (props[ZFS_PROP_USERREFS] == B_FALSE) + zhp->zfs_projected_props &= ~ZFS_PROJECTED_USERREFS; curr = nvlist_next_nvpair(zhp->zfs_props, NULL); diff --git a/lib/libzfs/libzfs_impl.h b/lib/libzfs/libzfs_impl.h index 6d7eca981d26..e4717289c205 100644 --- a/lib/libzfs/libzfs_impl.h +++ b/lib/libzfs/libzfs_impl.h @@ -82,8 +82,15 @@ struct zfs_handle { boolean_t zfs_mntcheck; char *zfs_mntopts; uint8_t *zfs_props_table; + uint64_t zfs_projected_creation; + uint64_t zfs_projected_userrefs; + uint8_t zfs_projected_props; + boolean_t zfs_projected_props_materialized; }; +#define ZFS_PROJECTED_CREATION (1U << 0) +#define ZFS_PROJECTED_USERREFS (1U << 1) + /* * Internal namespace property flags for selective remount via * mount_setattr(2). Passed to zfs_mount_setattr(). diff --git a/lib/libzfs/libzfs_iter.c b/lib/libzfs/libzfs_iter.c index 1844ce1624b3..71b9dc91151f 100644 --- a/lib/libzfs/libzfs_iter.c +++ b/lib/libzfs/libzfs_iter.c @@ -25,9 +25,11 @@ * Copyright (c) 2013, 2019 by Delphix. All rights reserved. * Copyright 2014 Nexenta Systems, Inc. All rights reserved. * Copyright (c) 2019 Datto Inc. + * Copyright (c) 2026, Wolfgang Hoschek */ #include +#include #include #include #include @@ -100,6 +102,531 @@ zfs_do_list_ioctl(zfs_handle_t *zhp, int arg, zfs_cmd_t *zc) return (rc); } +static int +zfs_batch_add_uint64_prop(nvlist_t *props, zfs_prop_t prop, uint64_t value) +{ + nvlist_t *propval; + int error; + + if ((error = nvlist_alloc(&propval, NV_UNIQUE_NAME, 0)) != 0) + return (error); + if ((error = nvlist_add_uint64(propval, ZPROP_VALUE, value)) == 0) { + error = nvlist_add_nvlist(props, zfs_prop_to_name(prop), + propval); + } + nvlist_free(propval); + return (error); +} + +/* + * Keep the userspace limit independent of the kernel tunable so a future + * kernel can increase its limit without exceeding this caller's destination + * buffer. A native packed result needs at most 264 bytes for its name (the + * string-array slot and ZFS_MAX_DATASET_NAME_LEN bytes), plus eight bytes for + * each uint64 array and one byte for each uint8 array. Reserving twice + * ZFS_MAX_DATASET_NAME_LEN bytes for each name, plus the array storage and + * 4 KiB for nvpair headers, alignment, and fixed metadata, is a conservative + * bound for every currently supported property combination. + */ +#define SNAPSHOT_LIST_BATCH_MAX_RESULTS 1024 +#define SNAPSHOT_LIST_BATCH_NVLIST_SIZE(num_uint64_arrays, num_uint8_arrays) \ + ((4 * 1024) + SNAPSHOT_LIST_BATCH_MAX_RESULTS * \ + (2 * ZFS_MAX_DATASET_NAME_LEN + \ + (num_uint64_arrays) * sizeof (uint64_t) + \ + (num_uint8_arrays) * sizeof (uint8_t))) + +static int +make_dataset_batch_handle(zfs_handle_t *pzhp, const char *snapname, + dmu_objset_type_t dmu_type, uint8_t dds_flags, + const uint64_t *createtxg, const uint64_t *guid, + const uint64_t *objsetid, const uint64_t *creation, + const uint64_t *userrefs, const uint64_t *numclones, + const uint64_t *used, const uint64_t *referenced, + const uint64_t *logicalreferenced, const uint64_t *written, + const uint8_t *inconsistent, const uint8_t *redacted, + const uint8_t *defer_destroy, + zfs_handle_t **result) +{ + zfs_handle_t *zhp = calloc(1, sizeof (zfs_handle_t)); + int error; + + *result = NULL; + if (zhp == NULL) + return (ENOMEM); + + zhp->zfs_hdl = pzhp->zfs_hdl; + zhp->zpool_hdl = zfs_get_pool_handle(pzhp); + zhp->zfs_type = ZFS_TYPE_SNAPSHOT; + zhp->zfs_head_type = dmu_type == DMU_OST_ZVOL ? + ZFS_TYPE_VOLUME : ZFS_TYPE_FILESYSTEM; + zhp->zfs_dmustats.dds_type = dmu_type; + zhp->zfs_dmustats.dds_is_snapshot = B_TRUE; + if (createtxg != NULL) + zhp->zfs_dmustats.dds_creation_txg = *createtxg; + if (guid != NULL) + zhp->zfs_dmustats.dds_guid = *guid; + if (numclones != NULL) + zhp->zfs_dmustats.dds_num_clones = *numclones; + if (inconsistent != NULL) + zhp->zfs_dmustats.dds_inconsistent = *inconsistent; + if (redacted != NULL) + zhp->zfs_dmustats.dds_redacted = *redacted; + zhp->zfs_dmustats.dds_flags = dds_flags; + if (creation != NULL) { + zhp->zfs_projected_creation = *creation; + zhp->zfs_projected_props |= ZFS_PROJECTED_CREATION; + } + if (userrefs != NULL) { + zhp->zfs_projected_userrefs = *userrefs; + zhp->zfs_projected_props |= ZFS_PROJECTED_USERREFS; + } + + if (strlcpy(zhp->zfs_name, pzhp->zfs_name, + sizeof (zhp->zfs_name)) >= sizeof (zhp->zfs_name) || + strlcat(zhp->zfs_name, "@", sizeof (zhp->zfs_name)) >= + sizeof (zhp->zfs_name) || + strlcat(zhp->zfs_name, snapname, sizeof (zhp->zfs_name)) >= + sizeof (zhp->zfs_name)) { + error = ENAMETOOLONG; + goto fail; + } + + if ((error = nvlist_alloc(&zhp->zfs_props, NV_UNIQUE_NAME, 0)) != 0 || + (error = nvlist_alloc(&zhp->zfs_user_props, NV_UNIQUE_NAME, 0)) != + 0) { + goto fail; + } + if ((used != NULL && (error = zfs_batch_add_uint64_prop( + zhp->zfs_props, ZFS_PROP_USED, *used)) != 0) || + (objsetid != NULL && (error = zfs_batch_add_uint64_prop( + zhp->zfs_props, ZFS_PROP_OBJSETID, *objsetid)) != 0) || + (referenced != NULL && (error = zfs_batch_add_uint64_prop( + zhp->zfs_props, ZFS_PROP_REFERENCED, *referenced)) != 0) || + (logicalreferenced != NULL && + (error = zfs_batch_add_uint64_prop(zhp->zfs_props, + ZFS_PROP_LOGICALREFERENCED, *logicalreferenced)) != 0) || + (written != NULL && (error = zfs_batch_add_uint64_prop( + zhp->zfs_props, ZFS_PROP_WRITTEN, *written)) != 0) || + (defer_destroy != NULL && (error = zfs_batch_add_uint64_prop( + zhp->zfs_props, ZFS_PROP_DEFER_DESTROY, *defer_destroy)) != 0)) { + goto fail; + } + + *result = zhp; + return (0); + +fail: + zfs_close(zhp); + return (error); +} + +static int +zfs_do_snapshot_list_batch_ioctl(zfs_handle_t *zhp, int flags, + uint64_t cursor, uint64_t min_txg, uint64_t max_txg, nvlist_t **result, + boolean_t *ioctl_eof) +{ + zfs_cmd_t zc = {"\0"}; + nvlist_t *args = fnvlist_alloc(); + nvlist_t *props = fnvlist_alloc(); + int error = 0; + + *result = NULL; + *ioctl_eof = B_FALSE; + + fnvlist_add_uint64(args, SNAP_ITER_BATCH_CURSOR, cursor); + fnvlist_add_uint64(args, SNAP_ITER_BATCH_MAX_RESULTS, + SNAPSHOT_LIST_BATCH_MAX_RESULTS); + if (min_txg != 0) + fnvlist_add_uint64(args, SNAP_ITER_MIN_TXG, min_txg); + if (max_txg != 0) + fnvlist_add_uint64(args, SNAP_ITER_MAX_TXG, max_txg); + if (flags & ZFS_ITER_BATCHED_CREATETXG) { + fnvlist_add_boolean(props, + zfs_prop_to_name(ZFS_PROP_CREATETXG)); + } + if (flags & ZFS_ITER_BATCHED_CREATION) { + fnvlist_add_boolean(props, + zfs_prop_to_name(ZFS_PROP_CREATION)); + } + if (flags & ZFS_ITER_BATCHED_GUID) + fnvlist_add_boolean(props, zfs_prop_to_name(ZFS_PROP_GUID)); + if (flags & ZFS_ITER_BATCHED_USERREFS) { + fnvlist_add_boolean(props, + zfs_prop_to_name(ZFS_PROP_USERREFS)); + } + if (flags & ZFS_ITER_BATCHED_NUMCLONES) { + fnvlist_add_boolean(props, + zfs_prop_to_name(ZFS_PROP_NUMCLONES)); + } + if (flags & ZFS_ITER_BATCHED_INCONSISTENT) { + fnvlist_add_boolean(props, + zfs_prop_to_name(ZFS_PROP_INCONSISTENT)); + } + if (flags & ZFS_ITER_BATCHED_REDACTED) { + fnvlist_add_boolean(props, + zfs_prop_to_name(ZFS_PROP_REDACTED)); + } + if (flags & ZFS_ITER_BATCHED_USED) + fnvlist_add_boolean(props, zfs_prop_to_name(ZFS_PROP_USED)); + if (flags & ZFS_ITER_BATCHED_REFERENCED) { + fnvlist_add_boolean(props, + zfs_prop_to_name(ZFS_PROP_REFERENCED)); + } + if (flags & ZFS_ITER_BATCHED_LOGICALREFERENCED) { + fnvlist_add_boolean(props, + zfs_prop_to_name(ZFS_PROP_LOGICALREFERENCED)); + } + if (flags & ZFS_ITER_BATCHED_DEFER_DESTROY) { + fnvlist_add_boolean(props, + zfs_prop_to_name(ZFS_PROP_DEFER_DESTROY)); + } + if (flags & ZFS_ITER_BATCHED_WRITTEN) + fnvlist_add_boolean(props, zfs_prop_to_name(ZFS_PROP_WRITTEN)); + if (flags & ZFS_ITER_BATCHED_OBJSETID) + fnvlist_add_boolean(props, zfs_prop_to_name(ZFS_PROP_OBJSETID)); + fnvlist_add_nvlist(args, SNAP_ITER_BATCH_PROPS, props); + + (void) strlcpy(zc.zc_name, zhp->zfs_name, sizeof (zc.zc_name)); + zcmd_write_src_nvlist(zhp->zfs_hdl, &zc, args); + zcmd_alloc_dst_nvlist(zhp->zfs_hdl, &zc, + SNAPSHOT_LIST_BATCH_NVLIST_SIZE(10, 4)); + for (;;) { + int ioctl_errno = 0; + + if (zfs_ioctl(zhp->zfs_hdl, ZFS_IOC_SNAPSHOT_LIST_BATCH, + &zc) != 0) { + ioctl_errno = errno; + } + /* + * An ioctl error does not imply that the output nvlist is + * empty. It can contain snapshots collected before the error. + */ + if (zc.zc_nvlist_dst_filled) { + if (zcmd_read_dst_nvlist(zhp->zfs_hdl, &zc, + result) != 0) { + nvlist_free(*result); + *result = NULL; + error = ENOMEM; + } else { + error = ioctl_errno; + /* + * Handler errors may return an empty + * output nvlist. + */ + if (error != 0 && nvlist_empty(*result)) { + nvlist_free(*result); + *result = NULL; + } + } + break; + } + if (ioctl_errno == ENOMEM) { + zcmd_expand_dst_nvlist(zhp->zfs_hdl, &zc); + continue; + } + error = ioctl_errno != 0 ? ioctl_errno : EPROTO; + break; + } + if (*result == NULL) + *ioctl_eof = error == ENOENT || error == ESRCH; + + zcmd_free_nvlists(&zc); + fnvlist_free(props); + fnvlist_free(args); + return (error); +} + +static int +zfs_iter_snapshots_batch(zfs_handle_t *zhp, int flags, zfs_iter_f func, + void *data, uint64_t min_txg, uint64_t max_txg, boolean_t *unavailable) +{ + uint64_t cursor = 0; + int ret; + boolean_t callback_invoked = B_FALSE; + + *unavailable = B_FALSE; + + for (;;) { + char **names = NULL; + uint64_t *createtxgs = NULL, *guids = NULL, *objsetids = NULL; + uint64_t *creations = NULL, *userrefs = NULL; + uint64_t *numclones = NULL, *used = NULL; + uint64_t *referenced = NULL, *logicalreferenced = NULL; + uint64_t *writtens = NULL; + uint8_t *inconsistent = NULL, *redacted = NULL; + uint8_t *defer_destroy = NULL, *written_valid = NULL; + uint_t count = 0, createtxg_count = 0, guid_count = 0; + uint_t objsetid_count = 0; + uint_t creation_count = 0, userref_count = 0; + uint_t numclone_count = 0, used_count = 0; + uint_t referenced_count = 0, logicalreferenced_count = 0; + uint_t inconsistent_count = 0, redacted_count = 0; + uint_t defer_destroy_count = 0, written_count = 0; + uint_t written_valid_count = 0; + uint64_t next_cursor, dmu_type, dds_flags; + nvlist_t *batch = NULL; + boolean_t eof, ioctl_eof; + int ioctl_errno; + + ret = zfs_do_snapshot_list_batch_ioctl(zhp, flags, cursor, + min_txg, max_txg, &batch, &ioctl_eof); + if (ioctl_eof) + return (0); + if (batch == NULL) { + if (ret == 0) + ret = EPROTO; + if (ret == ZFS_ERR_IOC_CMD_UNAVAIL || + ret == ZFS_ERR_IOC_ARG_UNAVAIL || ret == ENOTTY || + ret == ENOTSUP) { + if (callback_invoked == B_FALSE) { + *unavailable = B_TRUE; + return (ret); + } + } + errno = ret; + return (zfs_standard_error(zhp->zfs_hdl, errno, + dgettext(TEXT_DOMAIN, + "cannot iterate filesystems"))); + } + ioctl_errno = ret; + + eof = nvlist_exists(batch, SNAP_ITER_BATCH_EOF); + if (nvlist_lookup_uint64(batch, SNAP_ITER_BATCH_CURSOR, + &next_cursor) != 0) { + ret = EPROTO; + goto malformed; + } + if (nvlist_lookup_uint64(batch, SNAP_ITER_BATCH_DMU_TYPE, + &dmu_type) != 0 || + nvlist_lookup_uint64(batch, SNAP_ITER_BATCH_DDS_FLAGS, + &dds_flags) != 0 || + (dmu_type != DMU_OST_ZFS && dmu_type != DMU_OST_ZVOL) || + dds_flags > UINT8_MAX || + (dds_flags & DDS_FLAG_HAS_ENCRYPTED) == 0) { + ret = EPROTO; + goto malformed; + } + + ret = nvlist_lookup_string_array(batch, SNAP_ITER_BATCH_NAMES, + &names, &count); + if (ret == ENOENT) { + count = 0; + ret = 0; + } else if (ret != 0) { + ret = EPROTO; + goto malformed; + } + if (count > SNAPSHOT_LIST_BATCH_MAX_RESULTS) { + ret = EPROTO; + goto malformed; + } + if (count != 0 && (flags & ZFS_ITER_BATCHED_CREATETXG) && + (nvlist_lookup_uint64_array(batch, + SNAP_ITER_BATCH_CREATETXGS, &createtxgs, + &createtxg_count) != 0 || count != createtxg_count)) { + ret = EPROTO; + goto malformed; + } + if (count != 0 && (flags & ZFS_ITER_BATCHED_GUID) && + (nvlist_lookup_uint64_array(batch, SNAP_ITER_BATCH_GUIDS, + &guids, &guid_count) != 0 || count != guid_count)) { + ret = EPROTO; + goto malformed; + } + if (count != 0 && (flags & ZFS_ITER_BATCHED_OBJSETID) && + (nvlist_lookup_uint64_array(batch, + SNAP_ITER_BATCH_OBJSETIDS, &objsetids, + &objsetid_count) != 0 || count != objsetid_count)) { + ret = EPROTO; + goto malformed; + } + + if (count != 0 && (flags & ZFS_ITER_BATCHED_CREATION) && + (nvlist_lookup_uint64_array(batch, + SNAP_ITER_BATCH_CREATIONS, &creations, &creation_count) != + 0 || count != creation_count)) { + ret = EPROTO; + goto malformed; + } + if (count != 0 && (flags & ZFS_ITER_BATCHED_USERREFS) && + (nvlist_lookup_uint64_array(batch, + SNAP_ITER_BATCH_USERREF_COUNTS, &userrefs, + &userref_count) != 0 || count != userref_count)) { + ret = EPROTO; + goto malformed; + } + if (count != 0 && (flags & ZFS_ITER_BATCHED_NUMCLONES) && + (nvlist_lookup_uint64_array(batch, + SNAP_ITER_BATCH_NUMCLONES, &numclones, + &numclone_count) != 0 || count != numclone_count)) { + ret = EPROTO; + goto malformed; + } + if (count != 0 && (flags & ZFS_ITER_BATCHED_USED) && + (nvlist_lookup_uint64_array(batch, SNAP_ITER_BATCH_USED, + &used, &used_count) != 0 || count != used_count)) { + ret = EPROTO; + goto malformed; + } + if (count != 0 && (flags & ZFS_ITER_BATCHED_REFERENCED) && + (nvlist_lookup_uint64_array(batch, + SNAP_ITER_BATCH_REFERENCED, &referenced, + &referenced_count) != 0 || count != referenced_count)) { + ret = EPROTO; + goto malformed; + } + if (count != 0 && + (flags & ZFS_ITER_BATCHED_LOGICALREFERENCED) && + (nvlist_lookup_uint64_array(batch, + SNAP_ITER_BATCH_LOGICALREFERENCED, &logicalreferenced, + &logicalreferenced_count) != 0 || + count != logicalreferenced_count)) { + ret = EPROTO; + goto malformed; + } + if (count != 0 && (flags & ZFS_ITER_BATCHED_INCONSISTENT) && + (nvlist_lookup_uint8_array(batch, + SNAP_ITER_BATCH_INCONSISTENT, &inconsistent, + &inconsistent_count) != 0 || + count != inconsistent_count)) { + ret = EPROTO; + goto malformed; + } + if (count != 0 && (flags & ZFS_ITER_BATCHED_REDACTED) && + (nvlist_lookup_uint8_array(batch, + SNAP_ITER_BATCH_REDACTED, &redacted, + &redacted_count) != 0 || count != redacted_count)) { + ret = EPROTO; + goto malformed; + } + if (count != 0 && (flags & ZFS_ITER_BATCHED_DEFER_DESTROY) && + (nvlist_lookup_uint8_array(batch, + SNAP_ITER_BATCH_DEFER_DESTROY, &defer_destroy, + &defer_destroy_count) != 0 || + count != defer_destroy_count)) { + ret = EPROTO; + goto malformed; + } + if (count != 0 && (flags & ZFS_ITER_BATCHED_WRITTEN) && + (nvlist_lookup_uint64_array(batch, + SNAP_ITER_BATCH_WRITTENS, &writtens, &written_count) != 0 || + nvlist_lookup_uint8_array(batch, + SNAP_ITER_BATCH_WRITTEN_VALID, &written_valid, + &written_valid_count) != 0 || count != written_count || + count != written_valid_count)) { + ret = EPROTO; + goto malformed; + } + for (uint_t i = 0; i < count; i++) { + if (((flags & ZFS_ITER_BATCHED_INCONSISTENT) && + inconsistent[i] > 1) || + ((flags & ZFS_ITER_BATCHED_REDACTED) && + redacted[i] > 1) || + ((flags & ZFS_ITER_BATCHED_DEFER_DESTROY) && + defer_destroy[i] > 1) || + ((flags & ZFS_ITER_BATCHED_WRITTEN) && + written_valid[i] > 1)) { + ret = EPROTO; + goto malformed; + } + } + + for (uint_t i = 0; i < count; i++) { + const uint64_t *createtxg = + (flags & ZFS_ITER_BATCHED_CREATETXG) ? + &createtxgs[i] : NULL; + const uint64_t *guid = + (flags & ZFS_ITER_BATCHED_GUID) ? &guids[i] : NULL; + const uint64_t *objsetid = + (flags & ZFS_ITER_BATCHED_OBJSETID) ? + &objsetids[i] : NULL; + const uint64_t *creation = + (flags & ZFS_ITER_BATCHED_CREATION) ? + &creations[i] : NULL; + const uint64_t *userref = + (flags & ZFS_ITER_BATCHED_USERREFS) ? + &userrefs[i] : NULL; + const uint64_t *numclone = + (flags & ZFS_ITER_BATCHED_NUMCLONES) ? + &numclones[i] : NULL; + const uint64_t *used_value = + (flags & ZFS_ITER_BATCHED_USED) ? &used[i] : NULL; + const uint64_t *referenced_value = + (flags & ZFS_ITER_BATCHED_REFERENCED) ? + &referenced[i] : NULL; + const uint64_t *logicalreferenced_value = + (flags & ZFS_ITER_BATCHED_LOGICALREFERENCED) ? + &logicalreferenced[i] : NULL; + const uint64_t *written_value = + ((flags & ZFS_ITER_BATCHED_WRITTEN) && + written_valid[i]) ? &writtens[i] : NULL; + const uint8_t *inconsistent_value = + (flags & ZFS_ITER_BATCHED_INCONSISTENT) ? + &inconsistent[i] : NULL; + const uint8_t *redacted_value = + (flags & ZFS_ITER_BATCHED_REDACTED) ? + &redacted[i] : NULL; + const uint8_t *defer_destroy_value = + (flags & ZFS_ITER_BATCHED_DEFER_DESTROY) ? + &defer_destroy[i] : NULL; + zfs_handle_t *nzhp; + + ret = make_dataset_batch_handle(zhp, names[i], + (dmu_objset_type_t)dmu_type, (uint8_t)dds_flags, + createtxg, guid, objsetid, creation, userref, + numclone, + used_value, referenced_value, + logicalreferenced_value, written_value, + inconsistent_value, redacted_value, + defer_destroy_value, &nzhp); + if (ret != 0) { + int error = ret; + + nvlist_free(batch); + errno = error; + if (error == ENOMEM) + ret = no_memory(zhp->zfs_hdl); + else + ret = zfs_standard_error(zhp->zfs_hdl, + error, dgettext(TEXT_DOMAIN, + "cannot iterate filesystems")); + errno = error; + return (ret); + } + callback_invoked = B_TRUE; + if ((ret = func(nzhp, data)) != 0) { + nvlist_free(batch); + return (ret); + } + } + + nvlist_free(batch); + if (ioctl_errno != 0) { + if (ioctl_errno == ENOENT || ioctl_errno == ESRCH) + return (0); + errno = ioctl_errno; + return (zfs_standard_error(zhp->zfs_hdl, errno, + dgettext(TEXT_DOMAIN, + "cannot iterate filesystems"))); + } + if (eof) + return (0); + if (next_cursor == cursor) { + ret = EPROTO; + batch = NULL; + goto malformed; + } + cursor = next_cursor; + continue; + +malformed: + nvlist_free(batch); + errno = ret; + return (zfs_standard_error(zhp->zfs_hdl, errno, + dgettext(TEXT_DOMAIN, "cannot iterate filesystems"))); + } +} + /* * Iterate over all child filesystems */ @@ -165,11 +692,18 @@ zfs_iter_snapshots_v2(zfs_handle_t *zhp, int flags, zfs_iter_f func, zfs_cmd_t zc = {"\0"}; zfs_handle_t *nzhp; int ret; + boolean_t unavailable; nvlist_t *range_nvl = NULL; if (zhp->zfs_type == ZFS_TYPE_SNAPSHOT || zhp->zfs_type == ZFS_TYPE_BOOKMARK) return (0); + if (flags & ZFS_ITER_BATCHED) { + ret = zfs_iter_snapshots_batch(zhp, flags, func, data, min_txg, + max_txg, &unavailable); + if (!unavailable) + return (ret); + } zc.zc_simple = (flags & ZFS_ITER_SIMPLE) != 0; @@ -219,8 +753,8 @@ zfs_iter_bookmarks(zfs_handle_t *zhp, zfs_iter_f func, void *data) } int -zfs_iter_bookmarks_v2(zfs_handle_t *zhp, int flags __maybe_unused, - zfs_iter_f func, void *data) +zfs_iter_bookmarks_v2(zfs_handle_t *zhp, int flags, zfs_iter_f func, + void *data) { zfs_handle_t *nzhp; nvlist_t *props = NULL; @@ -233,12 +767,36 @@ zfs_iter_bookmarks_v2(zfs_handle_t *zhp, int flags __maybe_unused, /* Setup the requested properties nvlist. */ props = fnvlist_alloc(); - for (zfs_prop_t p = 0; p < ZFS_NUM_PROPS; p++) { - if (zfs_prop_valid_for_type(p, ZFS_TYPE_BOOKMARK, B_FALSE)) { - fnvlist_add_boolean(props, zfs_prop_to_name(p)); + if (flags & ZFS_ITER_BATCHED) { + if (flags & ZFS_ITER_BATCHED_GUID) { + fnvlist_add_boolean(props, + zfs_prop_to_name(ZFS_PROP_GUID)); + } + if (flags & ZFS_ITER_BATCHED_CREATETXG) { + fnvlist_add_boolean(props, + zfs_prop_to_name(ZFS_PROP_CREATETXG)); + } + if (flags & ZFS_ITER_BATCHED_CREATION) { + fnvlist_add_boolean(props, + zfs_prop_to_name(ZFS_PROP_CREATION)); + } + if (flags & ZFS_ITER_BATCHED_REFERENCED) { + fnvlist_add_boolean(props, + zfs_prop_to_name(ZFS_PROP_REFERENCED)); } + if (flags & ZFS_ITER_BATCHED_LOGICALREFERENCED) { + fnvlist_add_boolean(props, + zfs_prop_to_name(ZFS_PROP_LOGICALREFERENCED)); + } + } else { + for (zfs_prop_t p = 0; p < ZFS_NUM_PROPS; p++) { + if (zfs_prop_valid_for_type(p, ZFS_TYPE_BOOKMARK, + B_FALSE)) { + fnvlist_add_boolean(props, zfs_prop_to_name(p)); + } + } + fnvlist_add_boolean(props, "redact_complete"); } - fnvlist_add_boolean(props, "redact_complete"); if ((err = lzc_get_bookmarks(zhp->zfs_name, props, &bmarks)) != 0) goto out; @@ -346,6 +904,9 @@ zfs_iter_snapshots_sorted_v2(zfs_handle_t *zhp, int flags, zfs_iter_f callback, avl_create(&avl, zfs_snapshot_compare, sizeof (zfs_node_t), offsetof(zfs_node_t, zn_avlnode)); + /* zfs_snapshot_compare() requires the creation TXG. */ + if (flags & ZFS_ITER_BATCHED) + flags |= ZFS_ITER_BATCHED_CREATETXG; ret = zfs_iter_snapshots_v2(zhp, flags, zfs_sort_snaps, &avl, min_txg, max_txg); @@ -590,12 +1151,21 @@ zfs_iter_dependents(zfs_handle_t *zhp, boolean_t allowrecursion, return (zfs_iter_dependents_v2(zhp, 0, allowrecursion, func, data)); } +/* + * Iterate dependents with full snapshot handles because clone discovery reads + * properties which projected handles deliberately omit. + */ int zfs_iter_dependents_v2(zfs_handle_t *zhp, int flags, boolean_t allowrecursion, zfs_iter_f func, void *data) { iter_dependents_arg_t ida; - ida.flags = flags; + + /* + * Both batched and simple snapshot handles omit properties needed for + * clone discovery, so dependent traversal must use full handles. + */ + ida.flags = flags & ~(ZFS_ITER_BATCHED | ZFS_ITER_SIMPLE); ida.allowrecursion = allowrecursion; ida.stack = NULL; ida.func = func; diff --git a/lib/libzfs_core/libzfs_core.abi b/lib/libzfs_core/libzfs_core.abi index e88de56ae404..3706f7a2d416 100644 --- a/lib/libzfs_core/libzfs_core.abi +++ b/lib/libzfs_core/libzfs_core.abi @@ -2704,6 +2704,7 @@ + diff --git a/man/man4/zfs.4 b/man/man4/zfs.4 index cfa3bc3a5dfe..7f9c8856d5aa 100644 --- a/man/man4/zfs.4 +++ b/man/man4/zfs.4 @@ -1785,6 +1785,33 @@ Whether snapshot creation and destruction events are recorded in the pool history log, viewable with .Nm zpool Cm history . . +.It Sy zfs_snapshot_list_batch_size Ns = Ns Sy 1024 Pq uint +Maximum number of snapshots returned by one projected snapshot listing +batch. +The effective maximum is the smaller of this value and the limit requested by +userspace. +This limit is a proxy for the maximum transient memory consumed by a batch: +larger values can improve listing throughput at the cost of a larger memory +footprint. +Projected snapshot listing is cursor based: a batch stops after either this +many matching snapshots have been collected or the +.Sy zfs_snapshot_list_batch_time_us +budget has been reached, whichever occurs first. +It returns the matching entries collected so far and a cursor from which a +subsequent ioctl resumes, leaving the remaining snapshots for later batches. +The default is 1024 snapshots, and the supported range is 1 to 4096. +. +.It Sy zfs_snapshot_list_batch_time_us Ns = Ns Sy 10000 Pq uint +Soft wall-clock budget for the snapshot scan in one projected snapshot listing +batch. +This budget is a proxy for the maximum lock latency a batch can impose on +unrelated dataset operations. +The elapsed time is checked after each snapshot is examined by the scan. +At least one snapshot is therefore examined to ensure cursor progress, and a +single metadata operation may cause the budget to be exceeded. +The default is 10000 microseconds, and the supported range is 1 to 100000 +microseconds. +. .It Sy zfs_snapshot_no_setuid Ns = Ns Sy 0 Ns | Ns 1 Pq int Whether to disable .Em setuid/setgid diff --git a/module/os/freebsd/zfs/zfs_ioctl_os.c b/module/os/freebsd/zfs/zfs_ioctl_os.c index 29711fcf5d2c..395b119428a4 100644 --- a/module/os/freebsd/zfs/zfs_ioctl_os.c +++ b/module/os/freebsd/zfs/zfs_ioctl_os.c @@ -37,6 +37,40 @@ #include +/* Set a positive, allocation-bounded snapshot listing batch size. */ +int +param_set_snapshot_list_batch_size(SYSCTL_HANDLER_ARGS) +{ + uint_t value = zfs_snapshot_list_batch_size; + int error; + + error = sysctl_handle_int(oidp, &value, 0, req); + if (error != 0 || req->newptr == NULL) + return (SET_ERROR(error)); + if (value == 0 || value > ZFS_SNAPSHOT_LIST_BATCH_SIZE_MAX) + return (SET_ERROR(EINVAL)); + + zfs_snapshot_list_batch_size = value; + return (0); +} + +/* Set a positive, bounded snapshot listing wall-clock budget. */ +int +param_set_snapshot_list_batch_time_us(SYSCTL_HANDLER_ARGS) +{ + uint_t value = zfs_snapshot_list_batch_time_us; + int error; + + error = sysctl_handle_int(oidp, &value, 0, req); + if (error != 0 || req->newptr == NULL) + return (SET_ERROR(error)); + if (value == 0 || value > ZFS_SNAPSHOT_LIST_BATCH_TIME_US_MAX) + return (SET_ERROR(EINVAL)); + + zfs_snapshot_list_batch_time_us = value; + return (0); +} + int zfs_vfs_ref(zfsvfs_t **zfvp) { diff --git a/module/os/linux/zfs/zfs_ioctl_os.c b/module/os/linux/zfs/zfs_ioctl_os.c index ce6092be1da7..b65adc87bf6b 100644 --- a/module/os/linux/zfs/zfs_ioctl_os.c +++ b/module/os/linux/zfs/zfs_ioctl_os.c @@ -69,6 +69,40 @@ #include #include +static int +param_set_snapshot_list_batch_limit(const char *buf, zfs_kernel_param_t *kp, + uint_t maximum) +{ + uint_t value; + int error; + + error = kstrtouint(buf, 0, &value); + if (error < 0) + return (SET_ERROR(error)); + if (value == 0 || value > maximum) + return (SET_ERROR(-EINVAL)); + + error = param_set_uint(buf, kp); + return (error < 0 ? SET_ERROR(error) : 0); +} + +/* Set a positive, allocation-bounded snapshot listing batch size. */ +int +param_set_snapshot_list_batch_size(const char *buf, zfs_kernel_param_t *kp) +{ + return (param_set_snapshot_list_batch_limit(buf, kp, + ZFS_SNAPSHOT_LIST_BATCH_SIZE_MAX)); +} + +/* Set a positive, bounded snapshot listing wall-clock budget. */ +int +param_set_snapshot_list_batch_time_us(const char *buf, + zfs_kernel_param_t *kp) +{ + return (param_set_snapshot_list_batch_limit(buf, kp, + ZFS_SNAPSHOT_LIST_BATCH_TIME_US_MAX)); +} + boolean_t zfs_vfs_held(zfsvfs_t *zfsvfs) { diff --git a/module/zfs/dsl_dataset.c b/module/zfs/dsl_dataset.c index b7cdebc338d7..e9281cf4a9f9 100644 --- a/module/zfs/dsl_dataset.c +++ b/module/zfs/dsl_dataset.c @@ -2701,6 +2701,160 @@ dsl_get_written(dsl_dataset_t *ds, uint64_t *written) return (err); } +/* + * Validate the current deadlist, then calculate written bytes only when the + * snapshot is in the requested TXG range. Current deadlist errors are + * returned, while an excluded snapshot or predecessor error leaves written + * invalid. The configuration lock keeps the deadlist object and predecessor + * linkage stable. + */ +static int +dsl_dataset_snapshot_written(dsl_pool_t *dp, + const dsl_dataset_phys_t *dsp, uint64_t min_txg, uint64_t max_txg, + uint64_t *written, uint8_t *written_valid) +{ + objset_t *mos = dp->dp_meta_objset; + dmu_buf_t *dbuf; + dmu_object_info_t doi; + uint64_t deadlist_used, prev_referenced; + int error; + + ASSERT(dsl_pool_config_held(dp)); + *written_valid = B_FALSE; + + error = dmu_bonus_hold(mos, dsp->ds_deadlist_obj, FTAG, &dbuf); + if (error != 0) + return (error); + + dmu_object_info_from_db(dbuf, &doi); + if (doi.doi_type == DMU_OT_DEADLIST && + doi.doi_bonus_type == DMU_OT_DEADLIST_HDR && + doi.doi_bonus_size >= sizeof (dsl_deadlist_phys_t)) { + const dsl_deadlist_phys_t *dlp = dbuf->db_data; + + deadlist_used = dlp->dl_used; + } else if (doi.doi_type == DMU_OT_BPOBJ && + doi.doi_bonus_type == DMU_OT_BPOBJ_HDR && + doi.doi_bonus_size >= BPOBJ_SIZE_V0) { + const bpobj_phys_t *bpop = dbuf->db_data; + + deadlist_used = bpop->bpo_bytes; + } else { + error = SET_ERROR(EINVAL); + } + dmu_buf_rele(dbuf, FTAG); + if (error != 0) + return (error); + + if ((min_txg != 0 && dsp->ds_creation_txg < min_txg) || + (max_txg != 0 && dsp->ds_creation_txg > max_txg) || + dsp->ds_prev_snap_obj == 0) { + return (0); + } + + error = dmu_bonus_hold(mos, dsp->ds_prev_snap_obj, FTAG, &dbuf); + if (error != 0) + return (0); + + dmu_object_info_from_db(dbuf, &doi); + if (doi.doi_bonus_type != DMU_OT_DSL_DATASET || + doi.doi_bonus_size < sizeof (dsl_dataset_phys_t)) { + error = SET_ERROR(EINVAL); + } else { + const dsl_dataset_phys_t *prev_dsp = dbuf->db_data; + + if (prev_dsp->ds_creation_txg != dsp->ds_prev_snap_txg || + prev_dsp->ds_creation_txg >= dsp->ds_creation_txg) { + error = SET_ERROR(EINVAL); + } else { + prev_referenced = prev_dsp->ds_referenced_bytes; + } + } + dmu_buf_rele(dbuf, FTAG); + if (error != 0) + return (0); + + *written = dsp->ds_referenced_bytes; + *written -= prev_referenced; + *written += deadlist_used; + *written_valid = B_TRUE; + return (0); +} + +/* + * Retrieve the fields needed by projected snapshot listing without creating + * a dsl_dataset_t. The caller holds the pool configuration lock. + */ +int +dsl_dataset_snapshot_stats(dsl_pool_t *dp, uint64_t dsobj, + boolean_t want_userrefs, boolean_t want_redacted, boolean_t want_written, + uint64_t min_txg, uint64_t max_txg, dsl_dataset_snapshot_stats_t *stats) +{ + objset_t *mos = dp->dp_meta_objset; + dmu_buf_t *dbuf; + dmu_object_info_t doi; + dsl_dataset_phys_t *dsp; + uint64_t integer_size; + boolean_t zapified; + int error; + + ASSERT(dsl_pool_config_held(dp)); + memset(stats, 0, sizeof (*stats)); + + error = dmu_bonus_hold(mos, dsobj, FTAG, &dbuf); + if (error != 0) + return (error); + + dmu_object_info_from_db(dbuf, &doi); + zapified = doi.doi_type == DMU_OTN_ZAP_METADATA; + if (doi.doi_bonus_type != DMU_OT_DSL_DATASET || + doi.doi_bonus_size < sizeof (dsl_dataset_phys_t)) { + error = SET_ERROR(EINVAL); + goto out; + } + + dsp = dbuf->db_data; + stats->dss_creation_txg = dsp->ds_creation_txg; + stats->dss_creation_time = dsp->ds_creation_time; + stats->dss_guid = dsp->ds_guid; + stats->dss_num_clones = dsp->ds_num_children == 0 ? + 0 : dsp->ds_num_children - 1; + stats->dss_used = dsp->ds_unique_bytes; + stats->dss_referenced = dsp->ds_referenced_bytes; + stats->dss_logicalreferenced = dsp->ds_uncompressed_bytes; + stats->dss_inconsistent = + (dsp->ds_flags & DS_FLAG_INCONSISTENT) != 0; + stats->dss_defer_destroy = + (dsp->ds_flags & DS_FLAG_DEFER_DESTROY) != 0; + + if (want_userrefs && dsp->ds_userrefs_obj != 0) { + error = zap_count(mos, dsp->ds_userrefs_obj, + &stats->dss_userrefs); + if (error != 0) + goto out; + } + if (want_redacted && zapified) { + error = zap_length(mos, dsobj, + spa_feature_table[SPA_FEATURE_REDACTED_DATASETS].fi_guid, + &integer_size, NULL); + if (error == 0 && integer_size != sizeof (uint64_t)) + error = SET_ERROR(EINVAL); + if (error == 0) { + stats->dss_redacted = B_TRUE; + } else if (error == ENOENT) { + error = 0; + } + } + if (error == 0 && want_written) { + error = dsl_dataset_snapshot_written(dp, dsp, min_txg, max_txg, + &stats->dss_written, &stats->dss_written_valid); + } + +out: + dmu_buf_rele(dbuf, FTAG); + return (error); +} + /* * 'snap' should be a buffer of size ZFS_MAX_DATASET_NAME_LEN. */ diff --git a/module/zfs/zfs_ioctl.c b/module/zfs/zfs_ioctl.c index 2c68aa022a4c..54a38892d8b5 100644 --- a/module/zfs/zfs_ioctl.c +++ b/module/zfs/zfs_ioctl.c @@ -43,6 +43,7 @@ * Copyright (c) 2019, Allan Jude * Copyright 2026 Oxide Computer Company * Copyright (c) 2026, TrueNAS. + * Copyright (c) 2026, Wolfgang Hoschek */ /* @@ -2762,6 +2763,404 @@ zfs_ioc_dataset_list_next(zfs_cmd_t *zc) return (error); } +uint_t zfs_snapshot_list_batch_size = + ZFS_SNAPSHOT_LIST_BATCH_SIZE_DEFAULT; +uint_t zfs_snapshot_list_batch_time_us = + ZFS_SNAPSHOT_LIST_BATCH_TIME_US_DEFAULT; + +/* + * innvl: { + * (optional uint64) cursor + * (optional uint64) minimum creation txg + * (optional uint64) maximum creation txg + * (uint64) maximum number of returned snapshots + * (nvlist) requested properties, each named entry is a boolean + * } + * + * outnvl contains authoritative parent metadata, snapshot names, and one + * parallel array for each requested property. Result-count and elapsed-time + * limits amortize parent objset and ioctl overhead without holding the pool + * configuration lock for an unbounded snapshot walk. + */ +static const zfs_ioc_key_t zfs_keys_snapshot_list_batch[] = { + {SNAP_ITER_BATCH_PROPS, DATA_TYPE_NVLIST, 0}, + {SNAP_ITER_BATCH_MAX_RESULTS, DATA_TYPE_UINT64, 0}, + {SNAP_ITER_BATCH_CURSOR, DATA_TYPE_UINT64, ZK_OPTIONAL}, + {SNAP_ITER_MIN_TXG, DATA_TYPE_UINT64, ZK_OPTIONAL}, + {SNAP_ITER_MAX_TXG, DATA_TYPE_UINT64, ZK_OPTIONAL}, +}; + +static int +zfs_ioc_snapshot_list_batch(const char *fsname, nvlist_t *innvl, + nvlist_t *outnvl) +{ + char **names; + char *name_storage; + uint64_t *createtxgs = NULL, *guids = NULL, *objsetids = NULL; + uint64_t *creations = NULL; + uint64_t *userrefs = NULL, *numclones = NULL, *used = NULL; + uint64_t *referenced = NULL, *logicalreferenced = NULL; + uint64_t *writtens = NULL; + uint8_t *inconsistent = NULL, *redacted = NULL; + uint8_t *defer_destroy = NULL, *written_valid = NULL; + nvlist_t *props; + uint64_t cursor = 0, min_txg = 0, max_txg = 0, max_results; + uint_t batch_size; + uint_t batch_time_us = zfs_snapshot_list_batch_time_us; + uint_t count = 0; + hrtime_t start_time, time_budget; + boolean_t eof = B_FALSE; + boolean_t want_createtxg = B_FALSE, want_creation = B_FALSE; + boolean_t want_guid = B_FALSE, want_objsetid = B_FALSE; + boolean_t want_userrefs = B_FALSE; + boolean_t want_numclones = B_FALSE, want_inconsistent = B_FALSE; + boolean_t want_redacted = B_FALSE, want_used = B_FALSE; + boolean_t want_referenced = B_FALSE, want_logicalreferenced = B_FALSE; + boolean_t want_defer_destroy = B_FALSE, want_written = B_FALSE; + dmu_objset_type_t head_type; + uint8_t head_flags; + objset_t *os = NULL; + int error; + + VERIFY0(nvlist_lookup_uint64(innvl, SNAP_ITER_BATCH_MAX_RESULTS, + &max_results)); + if (max_results == 0) + return (SET_ERROR(EINVAL)); + batch_size = (uint_t)MIN((uint64_t)zfs_snapshot_list_batch_size, + max_results); + + VERIFY0(nvlist_lookup_nvlist(innvl, SNAP_ITER_BATCH_PROPS, &props)); + for (nvpair_t *pair = nvlist_next_nvpair(props, NULL); pair != NULL; + pair = nvlist_next_nvpair(props, pair)) { + if (nvpair_type(pair) != DATA_TYPE_BOOLEAN) + return (SET_ERROR(ZFS_ERR_IOC_ARG_BADTYPE)); + + switch (zfs_name_to_prop(nvpair_name(pair))) { + case ZFS_PROP_CREATETXG: + want_createtxg = B_TRUE; + break; + case ZFS_PROP_CREATION: + want_creation = B_TRUE; + break; + case ZFS_PROP_GUID: + want_guid = B_TRUE; + break; + case ZFS_PROP_OBJSETID: + want_objsetid = B_TRUE; + break; + case ZFS_PROP_USERREFS: + want_userrefs = B_TRUE; + break; + case ZFS_PROP_NUMCLONES: + want_numclones = B_TRUE; + break; + case ZFS_PROP_INCONSISTENT: + want_inconsistent = B_TRUE; + break; + case ZFS_PROP_REDACTED: + want_redacted = B_TRUE; + break; + case ZFS_PROP_USED: + want_used = B_TRUE; + break; + case ZFS_PROP_REFERENCED: + want_referenced = B_TRUE; + break; + case ZFS_PROP_LOGICALREFERENCED: + want_logicalreferenced = B_TRUE; + break; + case ZFS_PROP_DEFER_DESTROY: + want_defer_destroy = B_TRUE; + break; + case ZFS_PROP_WRITTEN: + want_written = B_TRUE; + break; + default: + return (SET_ERROR(ZFS_ERR_IOC_ARG_UNAVAIL)); + } + } + + (void) nvlist_lookup_uint64(innvl, SNAP_ITER_BATCH_CURSOR, &cursor); + (void) nvlist_lookup_uint64(innvl, SNAP_ITER_MIN_TXG, &min_txg); + (void) nvlist_lookup_uint64(innvl, SNAP_ITER_MAX_TXG, &max_txg); + + names = kmem_alloc(sizeof (names[0]) * batch_size, KM_SLEEP); + name_storage = vmem_alloc(ZFS_MAX_DATASET_NAME_LEN * batch_size, + KM_SLEEP); + for (uint_t i = 0; i < batch_size; i++) + names[i] = name_storage + ZFS_MAX_DATASET_NAME_LEN * i; + if (want_createtxg) { + createtxgs = kmem_alloc(sizeof (createtxgs[0]) * batch_size, + KM_SLEEP); + } + if (want_guid) + guids = kmem_alloc(sizeof (guids[0]) * batch_size, KM_SLEEP); + if (want_objsetid) { + objsetids = kmem_alloc(sizeof (objsetids[0]) * batch_size, + KM_SLEEP); + } + if (want_creation) { + creations = kmem_alloc(sizeof (creations[0]) * batch_size, + KM_SLEEP); + } + if (want_userrefs) { + userrefs = kmem_alloc(sizeof (userrefs[0]) * batch_size, + KM_SLEEP); + } + if (want_numclones) { + numclones = kmem_alloc(sizeof (numclones[0]) * batch_size, + KM_SLEEP); + } + if (want_inconsistent) { + inconsistent = kmem_alloc(sizeof (inconsistent[0]) * batch_size, + KM_SLEEP); + } + if (want_redacted) { + redacted = kmem_alloc(sizeof (redacted[0]) * batch_size, + KM_SLEEP); + } + if (want_used) + used = kmem_alloc(sizeof (used[0]) * batch_size, KM_SLEEP); + if (want_referenced) { + referenced = kmem_alloc(sizeof (referenced[0]) * batch_size, + KM_SLEEP); + } + if (want_logicalreferenced) { + logicalreferenced = kmem_alloc( + sizeof (logicalreferenced[0]) * batch_size, KM_SLEEP); + } + if (want_defer_destroy) { + defer_destroy = kmem_alloc( + sizeof (defer_destroy[0]) * batch_size, KM_SLEEP); + } + if (want_written) { + writtens = kmem_alloc(sizeof (writtens[0]) * batch_size, + KM_SLEEP); + written_valid = kmem_alloc( + sizeof (written_valid[0]) * batch_size, KM_SLEEP); + } + + time_budget = USEC2NSEC((hrtime_t)batch_time_us); + error = dmu_objset_hold(fsname, FTAG, &os); + if (error != 0) + goto out; + start_time = gethrtime(); + head_type = dmu_objset_type(os); + head_flags = DDS_FLAG_HAS_ENCRYPTED; + if (dmu_objset_ds(os)->ds_dir->dd_crypto_obj != 0) + head_flags |= DDS_FLAG_ENCRYPTED; + + while (count < batch_size) { + char snapname[ZFS_MAX_DATASET_NAME_LEN]; + dsl_dataset_snapshot_stats_t stats; + uint64_t obj, txg; + + if (issig()) { + error = SET_ERROR(EINTR); + break; + } + + error = dmu_snapshot_list_next(os, sizeof (snapname), snapname, + &obj, &cursor, NULL); + if (error == ENOENT) { + eof = B_TRUE; + error = 0; + break; + } else if (error != 0) { + break; + } + error = dsl_dataset_snapshot_stats(dmu_objset_pool(os), obj, + want_userrefs, want_redacted, want_written, min_txg, + max_txg, &stats); + /* + * Preserve public iterator partial-list results after a + * post-lookup ENOENT. Reporting EOF lets libzfs consume + * entries already in this batch instead of discarding it. + */ + if (error == ENOENT) { + eof = B_TRUE; + error = 0; + break; + } else if (error != 0) { + break; + } + + txg = stats.dss_creation_txg; + if ((min_txg == 0 || txg >= min_txg) && + (max_txg == 0 || txg <= max_txg)) { + (void) strlcpy(names[count], snapname, + ZFS_MAX_DATASET_NAME_LEN); + if (want_createtxg) + createtxgs[count] = txg; + if (want_guid) + guids[count] = stats.dss_guid; + if (want_objsetid) + objsetids[count] = obj; + if (want_creation) + creations[count] = stats.dss_creation_time; + if (want_userrefs) + userrefs[count] = stats.dss_userrefs; + if (want_numclones) + numclones[count] = stats.dss_num_clones; + if (want_inconsistent) + inconsistent[count] = stats.dss_inconsistent; + if (want_redacted) + redacted[count] = stats.dss_redacted; + if (want_used) + used[count] = stats.dss_used; + if (want_referenced) + referenced[count] = stats.dss_referenced; + if (want_logicalreferenced) { + logicalreferenced[count] = + stats.dss_logicalreferenced; + } + if (want_defer_destroy) + defer_destroy[count] = stats.dss_defer_destroy; + if (want_written) { + writtens[count] = stats.dss_written; + written_valid[count] = stats.dss_written_valid; + } + count++; + } + + if (gethrtime() - start_time >= time_budget) + break; + } + + dmu_objset_rele(os, FTAG); + os = NULL; + /* + * Return snapshots already collected when processing a later + * snapshot fails so libzfs can preserve iterator callback semantics. + */ + if (error == 0 || count != 0) { + fnvlist_add_uint64(outnvl, SNAP_ITER_BATCH_CURSOR, cursor); + fnvlist_add_uint64(outnvl, SNAP_ITER_BATCH_DMU_TYPE, + head_type); + fnvlist_add_uint64(outnvl, SNAP_ITER_BATCH_DDS_FLAGS, + head_flags); + if (eof) + fnvlist_add_boolean(outnvl, SNAP_ITER_BATCH_EOF); + if (count != 0) { + fnvlist_add_string_array(outnvl, SNAP_ITER_BATCH_NAMES, + (const char * const *)names, count); + if (want_createtxg) { + fnvlist_add_uint64_array(outnvl, + SNAP_ITER_BATCH_CREATETXGS, createtxgs, + count); + } + if (want_guid) { + fnvlist_add_uint64_array(outnvl, + SNAP_ITER_BATCH_GUIDS, guids, count); + } + if (want_objsetid) { + fnvlist_add_uint64_array(outnvl, + SNAP_ITER_BATCH_OBJSETIDS, objsetids, + count); + } + if (want_creation) { + fnvlist_add_uint64_array(outnvl, + SNAP_ITER_BATCH_CREATIONS, creations, + count); + } + if (want_userrefs) { + fnvlist_add_uint64_array(outnvl, + SNAP_ITER_BATCH_USERREF_COUNTS, userrefs, + count); + } + if (want_numclones) { + fnvlist_add_uint64_array(outnvl, + SNAP_ITER_BATCH_NUMCLONES, numclones, + count); + } + if (want_inconsistent) { + fnvlist_add_uint8_array(outnvl, + SNAP_ITER_BATCH_INCONSISTENT, inconsistent, + count); + } + if (want_redacted) { + fnvlist_add_uint8_array(outnvl, + SNAP_ITER_BATCH_REDACTED, redacted, count); + } + if (want_used) { + fnvlist_add_uint64_array(outnvl, + SNAP_ITER_BATCH_USED, used, count); + } + if (want_referenced) { + fnvlist_add_uint64_array(outnvl, + SNAP_ITER_BATCH_REFERENCED, referenced, + count); + } + if (want_logicalreferenced) { + fnvlist_add_uint64_array(outnvl, + SNAP_ITER_BATCH_LOGICALREFERENCED, + logicalreferenced, count); + } + if (want_defer_destroy) { + fnvlist_add_uint8_array(outnvl, + SNAP_ITER_BATCH_DEFER_DESTROY, + defer_destroy, count); + } + if (want_written) { + fnvlist_add_uint64_array(outnvl, + SNAP_ITER_BATCH_WRITTENS, writtens, count); + fnvlist_add_uint8_array(outnvl, + SNAP_ITER_BATCH_WRITTEN_VALID, + written_valid, count); + } + } + } + +out: + if (os != NULL) + dmu_objset_rele(os, FTAG); + vmem_free(name_storage, ZFS_MAX_DATASET_NAME_LEN * batch_size); + kmem_free(names, sizeof (names[0]) * batch_size); + if (want_createtxg) { + kmem_free(createtxgs, sizeof (createtxgs[0]) * batch_size); + } + if (want_guid) + kmem_free(guids, sizeof (guids[0]) * batch_size); + if (want_objsetid) { + kmem_free(objsetids, sizeof (objsetids[0]) * batch_size); + } + if (want_creation) { + kmem_free(creations, sizeof (creations[0]) * batch_size); + } + if (want_userrefs) { + kmem_free(userrefs, sizeof (userrefs[0]) * batch_size); + } + if (want_numclones) { + kmem_free(numclones, sizeof (numclones[0]) * batch_size); + } + if (want_inconsistent) { + kmem_free(inconsistent, + sizeof (inconsistent[0]) * batch_size); + } + if (want_redacted) + kmem_free(redacted, sizeof (redacted[0]) * batch_size); + if (want_used) + kmem_free(used, sizeof (used[0]) * batch_size); + if (want_referenced) { + kmem_free(referenced, sizeof (referenced[0]) * batch_size); + } + if (want_logicalreferenced) { + kmem_free(logicalreferenced, + sizeof (logicalreferenced[0]) * batch_size); + } + if (want_defer_destroy) { + kmem_free(defer_destroy, + sizeof (defer_destroy[0]) * batch_size); + } + if (want_written) { + kmem_free(writtens, sizeof (writtens[0]) * batch_size); + kmem_free(written_valid, + sizeof (written_valid[0]) * batch_size); + } + + return (error); +} + /* * inputs: * zc_name name of filesystem @@ -8088,6 +8487,11 @@ zfs_ioctl_init(void) zfs_ioc_get_bookmarks, zfs_secpolicy_read, DATASET_NAME, POOL_CHECK_SUSPENDED, B_FALSE, B_FALSE, zfs_keys_get_bookmarks, ARRAY_SIZE(zfs_keys_get_bookmarks)); + zfs_ioctl_register("snapshot_list_batch", ZFS_IOC_SNAPSHOT_LIST_BATCH, + zfs_ioc_snapshot_list_batch, zfs_secpolicy_read, DATASET_NAME, + POOL_CHECK_SUSPENDED, B_FALSE, B_FALSE, + zfs_keys_snapshot_list_batch, + ARRAY_SIZE(zfs_keys_snapshot_list_batch)); zfs_ioctl_register("get_bookmark_props", ZFS_IOC_GET_BOOKMARK_PROPS, zfs_ioc_get_bookmark_props, zfs_secpolicy_read, ENTITY_NAME, @@ -8896,3 +9300,11 @@ ZFS_MODULE_PARAM(zfs, zfs_, max_nvlist_src_size, U64, ZMOD_RW, ZFS_MODULE_PARAM(zfs, zfs_, history_output_max, U64, ZMOD_RW, "Maximum size in bytes of ZFS ioctl output that will be logged"); + +ZFS_MODULE_PARAM_CALL(zfs, zfs_, snapshot_list_batch_size, + param_set_snapshot_list_batch_size, param_get_uint, ZMOD_RW, + "Maximum snapshots returned per projected listing batch"); + +ZFS_MODULE_PARAM_CALL(zfs, zfs_, snapshot_list_batch_time_us, + param_set_snapshot_list_batch_time_us, param_get_uint, ZMOD_RW, + "Soft projected snapshot listing wall-clock budget in us (1-100000)"); diff --git a/tests/runfiles/common.run b/tests/runfiles/common.run index 51559f185606..562d03ac6a40 100644 --- a/tests/runfiles/common.run +++ b/tests/runfiles/common.run @@ -257,7 +257,9 @@ tests = ['zfs_inherit_001_neg', 'zfs_inherit_002_neg', 'zfs_inherit_003_pos', tags = ['functional', 'cli_root', 'zfs_inherit'] [tests/functional/cli_root/zfs_list] -tests = ['zfs_list_009_pos'] +tests = ['zfs_list_009_pos', 'zfs_list_013_pos', 'zfs_list_014_pos', + 'zfs_list_010_pos', 'zfs_list_011_pos', 'zfs_list_012_pos', + 'zfs_list_016_pos', 'zfs_list_017_pos'] tags = ['functional', 'cli_root', 'zfs_list'] [tests/functional/cli_root/zfs_load-key] diff --git a/tests/zfs-tests/cmd/.gitignore b/tests/zfs-tests/cmd/.gitignore index d55bf5daccb9..e1afd5fed540 100644 --- a/tests/zfs-tests/cmd/.gitignore +++ b/tests/zfs-tests/cmd/.gitignore @@ -41,6 +41,8 @@ /rename_dir /rm_lnkcnt_zero_file /send_doall +/snapshot_list_test +/snapshot_list_stats_test /statx /stride_dd /threadsappend diff --git a/tests/zfs-tests/cmd/Makefile.am b/tests/zfs-tests/cmd/Makefile.am index b7adf6de7600..0106fcfd547f 100644 --- a/tests/zfs-tests/cmd/Makefile.am +++ b/tests/zfs-tests/cmd/Makefile.am @@ -70,6 +70,27 @@ scripts_zfs_tests_bin_PROGRAMS += %D%/libzfs_mnttab_cache_check %C%_libzfs_mnttab_cache_check_LDADD = \ libzfs.la +scripts_zfs_tests_bin_PROGRAMS += %D%/snapshot_list_test +%C%_snapshot_list_test_LDADD = \ + libzfs.la \ + libnvpair.la + +scripts_zfs_tests_bin_PROGRAMS += %D%/snapshot_list_stats_test +%C%_snapshot_list_stats_test_CPPFLAGS = $(AM_CPPFLAGS) $(LIBZPOOL_CPPFLAGS) +# Keep dmu_bonus_hold() wrappable with -Bsymbolic-functions or --disable-static. +%C%_snapshot_list_stats_test_LDFLAGS = -Wl,--wrap=dmu_bonus_hold +%C%_snapshot_list_stats_test_LDADD = \ + module/zfs/libzpool_la-dsl_dataset.lo \ + libzpool.la \ + libnvpair.la + +scripts_zfs_tests_bin_LTLIBRARIES = %D%/libsnapshot_list_test_shim.la +%C%_libsnapshot_list_test_shim_la_LDFLAGS = -module -avoid-version +%C%_libsnapshot_list_test_shim_la_LIBADD = libnvpair.la +if BUILD_LINUX +%C%_libsnapshot_list_test_shim_la_LIBADD += -ldl +endif + scripts_zfs_tests_bin_PROGRAMS += %D%/manipulate_user_buffer scripts_zfs_tests_bin_PROGRAMS += %D%/mkbusy %D%/mkfile %D%/mkfiles %D%/mktree diff --git a/tests/zfs-tests/cmd/libsnapshot_list_test_shim.c b/tests/zfs-tests/cmd/libsnapshot_list_test_shim.c new file mode 100644 index 000000000000..815d5772b980 --- /dev/null +++ b/tests/zfs-tests/cmd/libsnapshot_list_test_shim.c @@ -0,0 +1,524 @@ +// SPDX-License-Identifier: CDDL-1.0 +/* + * Trace or inject errors into projected snapshot listing. This library is + * only loaded explicitly by ZTS tests. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +typedef int (*lzc_ioctl_fd_fn_t)(int, unsigned long, zfs_cmd_t *); +typedef int (*lzc_get_bookmarks_fn_t)(const char *, nvlist_t *, nvlist_t **); +typedef int (*nvlist_add_nvlist_fn_t)(nvlist_t *, const char *, + const nvlist_t *); + +static int handle_enomem_armed; +static int handle_enomem_injected; +static int empty_batch_injected; +static int real_enomem_injected; + +static int +write_all(int fd, const char *buffer, size_t length) +{ + ssize_t written; + + while (length > 0) { + written = write(fd, buffer, length); + if (written > 0) { + buffer += written; + length -= written; + continue; + } + if (written == -1 && errno == EINTR) + continue; + return (-1); + } + return (0); +} + +static lzc_ioctl_fd_fn_t +find_lzc_ioctl_fd(void) +{ + void *symbol = dlsym(RTLD_NEXT, "lzc_ioctl_fd"); + lzc_ioctl_fd_fn_t function; + + (void) memcpy(&function, &symbol, sizeof (function)); + return (function); +} + +static lzc_get_bookmarks_fn_t +find_lzc_get_bookmarks(void) +{ + void *symbol = dlsym(RTLD_NEXT, "lzc_get_bookmarks"); + lzc_get_bookmarks_fn_t function; + + (void) memcpy(&function, &symbol, sizeof (function)); + return (function); +} + +static nvlist_add_nvlist_fn_t +find_nvlist_add_nvlist(void) +{ + void *symbol = dlsym(RTLD_NEXT, "nvlist_add_nvlist"); + nvlist_add_nvlist_fn_t function; + + (void) memcpy(&function, &symbol, sizeof (function)); + return (function); +} + +static void +write_marker(const char *mode) +{ + const char *path; + int fd; + int saved_errno = errno; + + path = getenv("ZFS_SNAPSHOT_LIST_TEST_MARKER"); + if (path != NULL) { + fd = open(path, O_WRONLY | O_CREAT | O_APPEND, 0600); + if (fd >= 0) { + if (write_all(fd, mode, strlen(mode)) == 0) + (void) write_all(fd, "\n", 1); + (void) close(fd); + } + } + errno = saved_errno; +} + +static int +pack_batch(zfs_cmd_t *zc, nvlist_t *batch) +{ + char *packed = NULL; + size_t packed_size = 0; + int error; + + error = nvlist_pack(batch, &packed, &packed_size, NV_ENCODE_NATIVE, 0); + if (error != 0) + goto out; + if (packed_size > zc->zc_nvlist_dst_size) { + error = EPROTO; + goto out; + } + + (void) memcpy((void *)(uintptr_t)zc->zc_nvlist_dst, packed, + packed_size); + zc->zc_nvlist_dst_size = packed_size; + zc->zc_nvlist_dst_filled = B_TRUE; + +out: + free(packed); + return (error); +} + +static boolean_t +batch_has_snapshot_results(const zfs_cmd_t *zc) +{ + nvlist_t *batch = NULL; + char **names; + uint_t count; + boolean_t result = B_FALSE; + int saved_errno = errno; + + if (zc->zc_nvlist_dst_filled && zc->zc_nvlist_dst != 0 && + zc->zc_nvlist_dst_size != 0 && + nvlist_unpack((char *)(uintptr_t)zc->zc_nvlist_dst, + zc->zc_nvlist_dst_size, &batch, 0) == 0 && + nvlist_lookup_string_array(batch, SNAP_ITER_BATCH_NAMES, &names, + &count) == 0 && count != 0) { + result = B_TRUE; + } + + nvlist_free(batch); + errno = saved_errno; + return (result); +} + +/* + * Preserve the kernel's opaque cursor while omitting this batch's results. + * This deterministically exercises continuation after an empty, non-EOF reply. + */ +static int +replace_with_empty_batch(zfs_cmd_t *zc) +{ + nvlist_t *batch = NULL; + nvlist_t *replacement = NULL; + uint64_t cursor, dmu_type, dds_flags; + int error; + + error = nvlist_unpack((void *)(uintptr_t)zc->zc_nvlist_dst, + zc->zc_nvlist_dst_size, &batch, 0); + if (error != 0) + goto out; + if (nvlist_exists(batch, SNAP_ITER_BATCH_EOF) || + nvlist_lookup_uint64(batch, SNAP_ITER_BATCH_CURSOR, &cursor) != 0 || + nvlist_lookup_uint64(batch, SNAP_ITER_BATCH_DMU_TYPE, + &dmu_type) != 0 || + nvlist_lookup_uint64(batch, SNAP_ITER_BATCH_DDS_FLAGS, + &dds_flags) != 0) { + error = EPROTO; + goto out; + } + + error = nvlist_alloc(&replacement, NV_UNIQUE_NAME, 0); + if (error != 0) + goto out; + error = nvlist_add_uint64(replacement, SNAP_ITER_BATCH_CURSOR, cursor); + if (error != 0) + goto out; + error = nvlist_add_uint64(replacement, SNAP_ITER_BATCH_DMU_TYPE, + dmu_type); + if (error != 0) + goto out; + error = nvlist_add_uint64(replacement, SNAP_ITER_BATCH_DDS_FLAGS, + dds_flags); + if (error != 0) + goto out; + error = pack_batch(zc, replacement); + +out: + nvlist_free(replacement); + nvlist_free(batch); + return (error); +} + +static boolean_t +is_metadata_mode(const char *mode) +{ + return (strcmp(mode, "missing_dmu_type") == 0 || + strcmp(mode, "invalid_dmu_type") == 0 || + strcmp(mode, "missing_dds_flags") == 0 || + strcmp(mode, "invalid_dds_flags") == 0); +} + +static boolean_t +is_written_metadata_mode(const char *mode) +{ + return (strcmp(mode, "missing_writtens") == 0 || + strcmp(mode, "missing_written_valid") == 0 || + strcmp(mode, "short_writtens") == 0 || + strcmp(mode, "short_written_valid") == 0 || + strcmp(mode, "invalid_written_valid") == 0); +} + +static int +replace_batch_metadata(zfs_cmd_t *zc, const char *mode) +{ + nvlist_t *batch = NULL; + const char *name; + boolean_t invalid; + int error; + + if (strcmp(mode, "missing_dmu_type") == 0 || + strcmp(mode, "invalid_dmu_type") == 0) { + name = SNAP_ITER_BATCH_DMU_TYPE; + invalid = strcmp(mode, "invalid_dmu_type") == 0; + } else { + name = SNAP_ITER_BATCH_DDS_FLAGS; + invalid = strcmp(mode, "invalid_dds_flags") == 0; + } + + error = nvlist_unpack((void *)(uintptr_t)zc->zc_nvlist_dst, + zc->zc_nvlist_dst_size, &batch, 0); + if (error != 0) + goto out; + (void) nvlist_remove_all(batch, name); + if (invalid && + (error = nvlist_add_uint64(batch, name, UINT64_MAX)) != 0) { + goto out; + } + error = pack_batch(zc, batch); + +out: + nvlist_free(batch); + return (error); +} + +static int +replace_written_metadata(zfs_cmd_t *zc, const char *mode) +{ + nvlist_t *batch = NULL; + uint64_t *writtens, *writtens_copy = NULL; + uint8_t *valid, *valid_copy = NULL; + uint_t count; + int error; + + error = nvlist_unpack((void *)(uintptr_t)zc->zc_nvlist_dst, + zc->zc_nvlist_dst_size, &batch, 0); + if (error != 0) + goto out; + + if (strcmp(mode, "missing_writtens") == 0) { + (void) nvlist_remove_all(batch, SNAP_ITER_BATCH_WRITTENS); + } else if (strcmp(mode, "missing_written_valid") == 0) { + (void) nvlist_remove_all(batch, SNAP_ITER_BATCH_WRITTEN_VALID); + } else if (strcmp(mode, "short_writtens") == 0) { + error = nvlist_lookup_uint64_array(batch, + SNAP_ITER_BATCH_WRITTENS, &writtens, &count); + if (error != 0 || count < 2) { + error = EPROTO; + goto out; + } + writtens_copy = malloc(sizeof (writtens_copy[0]) * count); + if (writtens_copy == NULL) { + error = ENOMEM; + goto out; + } + (void) memcpy(writtens_copy, writtens, + sizeof (writtens_copy[0]) * count); + (void) nvlist_remove_all(batch, SNAP_ITER_BATCH_WRITTENS); + error = nvlist_add_uint64_array(batch, SNAP_ITER_BATCH_WRITTENS, + writtens_copy, count - 1); + if (error != 0) + goto out; + } else { + error = nvlist_lookup_uint8_array(batch, + SNAP_ITER_BATCH_WRITTEN_VALID, &valid, &count); + if (error != 0 || count < 2) { + error = EPROTO; + goto out; + } + valid_copy = malloc(sizeof (valid_copy[0]) * count); + if (valid_copy == NULL) { + error = ENOMEM; + goto out; + } + (void) memcpy(valid_copy, valid, + sizeof (valid_copy[0]) * count); + (void) nvlist_remove_all(batch, SNAP_ITER_BATCH_WRITTEN_VALID); + if (strcmp(mode, "invalid_written_valid") == 0) + valid_copy[0] = 2; + else + count--; + error = nvlist_add_uint8_array(batch, + SNAP_ITER_BATCH_WRITTEN_VALID, valid_copy, count); + if (error != 0) + goto out; + } + + error = pack_batch(zc, batch); + +out: + free(valid_copy); + free(writtens_copy); + nvlist_free(batch); + return (error); +} + +static boolean_t +matches_target(const char *name, const char *target) +{ + return (target == NULL || strcmp(name, target) == 0); +} + +static boolean_t +is_default_projected_bookmark_props(nvlist_t *props) +{ + nvpair_t *pair = NULL; + unsigned int count = 0; + + while ((pair = nvlist_next_nvpair(props, pair)) != NULL) + count++; + return (count == 1 && + nvlist_exists(props, zfs_prop_to_name(ZFS_PROP_REFERENCED))); +} + +int +lzc_get_bookmarks(const char *fsname, nvlist_t *props, nvlist_t **bmarks) +{ + static lzc_get_bookmarks_fn_t next; + const char *mode = getenv("ZFS_SNAPSHOT_LIST_TEST_MODE"); + const char *target = getenv("ZFS_SNAPSHOT_LIST_TEST_TARGET"); + int error = 0; + + if (mode != NULL && strcmp(mode, "bookmark_eio") == 0) + error = EIO; + else if (mode != NULL && strcmp(mode, "bookmark_enoent") == 0) + error = ENOENT; + else if (mode != NULL && strcmp(mode, "bookmark_esrch") == 0) + error = ESRCH; + if (error != 0) { + write_marker(mode); + return (error); + } + if (mode != NULL && strcmp(mode, "bookmark_projected") == 0 && + matches_target(fsname, target)) { + if (!is_default_projected_bookmark_props(props)) { + write_marker("bookmark_not_projected"); + return (EPROTO); + } + write_marker(mode); + } + if (mode != NULL && strcmp(mode, "bookmark_empty_projected") == 0 && + matches_target(fsname, target)) { + if (nvlist_next_nvpair(props, NULL) != NULL) { + write_marker("bookmark_not_empty_projected"); + return (EPROTO); + } + write_marker(mode); + } + + if (next == NULL) + next = find_lzc_get_bookmarks(); + if (next == NULL) + return (ENOSYS); + return (next(fsname, props, bmarks)); +} + +int +nvlist_add_nvlist(nvlist_t *nvl, const char *name, const nvlist_t *val) +{ + static nvlist_add_nvlist_fn_t next; + const char *mode = getenv("ZFS_SNAPSHOT_LIST_TEST_MODE"); + + if (handle_enomem_armed && mode != NULL) { + if (!handle_enomem_injected && + strcmp(mode, "handle_enomem") == 0 && + strcmp(name, "userrefs") == 0) { + handle_enomem_injected = 1; + write_marker(mode); + return (ENOMEM); + } + if (strcmp(mode, "direct_properties") == 0 && + (strcmp(name, "creation") == 0 || + strcmp(name, "userrefs") == 0)) { + write_marker("unexpected_property_nvlist"); + return (ENOMEM); + } + } + + if (next == NULL) + next = find_nvlist_add_nvlist(); + if (next == NULL) + return (ENOSYS); + return (next(nvl, name, val)); +} + +int +lzc_ioctl_fd(int fd, unsigned long request, zfs_cmd_t *zc) +{ + static lzc_ioctl_fd_fn_t next; + static int enomem_injected; + static unsigned int batch_calls; + const char *mode = getenv("ZFS_SNAPSHOT_LIST_TEST_MODE"); + int error; + int injected_errno = 0; + + if (request == ZFS_IOC_SNAPSHOT_LIST_BATCH && mode != NULL) { + batch_calls++; + if (strcmp(mode, "count") == 0) + write_marker(mode); + else if (strcmp(mode, "enotty") == 0) + injected_errno = ENOTTY; + else if (strcmp(mode, "enotsup") == 0) + injected_errno = ENOTSUP; + else if (strcmp(mode, "enotsup_after_first") == 0 && + batch_calls > 1) + injected_errno = ENOTSUP; + else if (strcmp(mode, "enoent_after_first") == 0 && + batch_calls > 1) + injected_errno = ENOENT; + else if (strcmp(mode, "esrch_after_first") == 0 && + batch_calls > 1) + injected_errno = ESRCH; + else if (strcmp(mode, "cmd_unavail") == 0) + injected_errno = ZFS_ERR_IOC_CMD_UNAVAIL; + else if (strcmp(mode, "arg_unavail") == 0) + injected_errno = ZFS_ERR_IOC_ARG_UNAVAIL; + else if (strcmp(mode, "enoent") == 0) + injected_errno = ENOENT; + else if (strcmp(mode, "esrch") == 0) + injected_errno = ESRCH; + else if (strcmp(mode, "eintr") == 0) + injected_errno = EINTR; + + if (injected_errno != 0) { + write_marker(mode); + errno = injected_errno; + return (-1); + } + if (strcmp(mode, "enomem") == 0 && !enomem_injected) { + enomem_injected = 1; + zc->zc_nvlist_dst_size *= 2; + write_marker(mode); + errno = ENOMEM; + return (-1); + } + } + + if (next == NULL) + next = find_lzc_ioctl_fd(); + if (next == NULL) { + errno = ENOSYS; + return (-1); + } + if (request == ZFS_IOC_SNAPSHOT_LIST_BATCH && mode != NULL && + strcmp(mode, "real_enomem") == 0 && !real_enomem_injected) { + /* + * Keep the allocation, but advertise too little room for + * copyout. + */ + real_enomem_injected = 1; + zc->zc_nvlist_dst_size = 1; + error = next(fd, request, zc); + if (error == -1 && errno == ENOMEM && + zc->zc_nvlist_dst_size > 1) { + write_marker(mode); + return (error); + } + errno = EPROTO; + return (-1); + } + error = next(fd, request, zc); + if (error == -1 && errno == EIO && + request == ZFS_IOC_SNAPSHOT_LIST_BATCH && mode != NULL && + strcmp(mode, "partial_eio_output") == 0 && + batch_has_snapshot_results(zc)) { + write_marker(mode); + } + if (error == 0 && request == ZFS_IOC_SNAPSHOT_LIST_BATCH && + mode != NULL && strcmp(mode, "empty_non_eof") == 0 && + !empty_batch_injected) { + empty_batch_injected = 1; + error = replace_with_empty_batch(zc); + if (error != 0) { + errno = error; + return (-1); + } + write_marker(mode); + } + if (error == 0 && request == ZFS_IOC_SNAPSHOT_LIST_BATCH && + mode != NULL && is_metadata_mode(mode)) { + error = replace_batch_metadata(zc, mode); + if (error != 0) { + errno = error; + return (-1); + } + write_marker(mode); + } + if (error == 0 && request == ZFS_IOC_SNAPSHOT_LIST_BATCH && + mode != NULL && is_written_metadata_mode(mode)) { + error = replace_written_metadata(zc, mode); + if (error != 0) { + errno = error; + return (-1); + } + write_marker(mode); + } + if (error == 0 && request == ZFS_IOC_SNAPSHOT_LIST_BATCH && + mode != NULL && (strcmp(mode, "handle_enomem") == 0 || + strcmp(mode, "direct_properties") == 0)) { + handle_enomem_armed = 1; + if (strcmp(mode, "direct_properties") == 0) + write_marker(mode); + } + return (error); +} diff --git a/tests/zfs-tests/cmd/libzfs_input_check.c b/tests/zfs-tests/cmd/libzfs_input_check.c index 3c9d369b1e54..f951f0948dde 100644 --- a/tests/zfs-tests/cmd/libzfs_input_check.c +++ b/tests/zfs-tests/cmd/libzfs_input_check.c @@ -561,6 +561,69 @@ test_get_bookmarks(const char *dataset) nvlist_free(optional); } +static void +test_snapshot_list_batch(const char *dataset) +{ + nvlist_t *required = fnvlist_alloc(); + nvlist_t *optional = fnvlist_alloc(); + nvlist_t *props = fnvlist_alloc(); + nvlist_t *input = fnvlist_alloc(); + + fnvlist_add_uint64(optional, SNAP_ITER_BATCH_CURSOR, 0); + fnvlist_add_uint64(optional, SNAP_ITER_MIN_TXG, 0); + fnvlist_add_uint64(optional, SNAP_ITER_MAX_TXG, UINT64_MAX); + fnvlist_add_boolean(props, "createtxg"); + fnvlist_add_boolean(props, "creation"); + fnvlist_add_boolean(props, "guid"); + fnvlist_add_boolean(props, "inconsistent"); + fnvlist_add_boolean(props, "numclones"); + fnvlist_add_boolean(props, "redacted"); + fnvlist_add_boolean(props, "used"); + fnvlist_add_boolean(props, "referenced"); + fnvlist_add_boolean(props, "logicalreferenced"); + fnvlist_add_boolean(props, "defer_destroy"); + fnvlist_add_boolean(props, "written"); + fnvlist_add_boolean(props, "objsetid"); + fnvlist_add_boolean(props, "userrefs"); + fnvlist_add_uint64(required, SNAP_ITER_BATCH_MAX_RESULTS, 1024); + fnvlist_add_nvlist(required, SNAP_ITER_BATCH_PROPS, props); + + IOC_INPUT_TEST(ZFS_IOC_SNAPSHOT_LIST_BATCH, dataset, required, optional, + 0); + + fnvlist_free(props); + props = fnvlist_alloc(); + fnvlist_add_nvlist(input, SNAP_ITER_BATCH_PROPS, props); + lzc_ioctl_run(ZFS_IOC_SNAPSHOT_LIST_BATCH, dataset, input, + ZFS_ERR_IOC_ARG_REQUIRED); + fnvlist_add_uint64(input, SNAP_ITER_BATCH_MAX_RESULTS, 1024); + lzc_ioctl_run(ZFS_IOC_SNAPSHOT_LIST_BATCH, dataset, input, 0); + fnvlist_remove(input, SNAP_ITER_BATCH_MAX_RESULTS); + fnvlist_add_uint64(input, SNAP_ITER_BATCH_MAX_RESULTS, 0); + lzc_ioctl_run(ZFS_IOC_SNAPSHOT_LIST_BATCH, dataset, input, EINVAL); + fnvlist_remove(input, SNAP_ITER_BATCH_MAX_RESULTS); + fnvlist_add_uint64(input, SNAP_ITER_BATCH_MAX_RESULTS, 1024); + + fnvlist_add_boolean(props, "available"); + fnvlist_remove(input, SNAP_ITER_BATCH_PROPS); + fnvlist_add_nvlist(input, SNAP_ITER_BATCH_PROPS, props); + lzc_ioctl_run(ZFS_IOC_SNAPSHOT_LIST_BATCH, dataset, input, + ZFS_ERR_IOC_ARG_UNAVAIL); + + fnvlist_free(props); + props = fnvlist_alloc(); + fnvlist_add_string(props, "guid", "bogus"); + fnvlist_remove(input, SNAP_ITER_BATCH_PROPS); + fnvlist_add_nvlist(input, SNAP_ITER_BATCH_PROPS, props); + lzc_ioctl_run(ZFS_IOC_SNAPSHOT_LIST_BATCH, dataset, input, + ZFS_ERR_IOC_ARG_BADTYPE); + + nvlist_free(input); + nvlist_free(props); + nvlist_free(optional); + nvlist_free(required); +} + static void test_destroy_bookmarks(const char *pool, const char *bookmark) { @@ -1029,6 +1092,7 @@ zfs_ioc_input_tests(const char *pool) test_bookmark(pool, snapshot, bookmark); test_get_bookmarks(dataset); + test_snapshot_list_batch(dataset); test_get_bookmark_props(bookmark); test_destroy_bookmarks(pool, bookmark); @@ -1220,6 +1284,7 @@ validate_ioc_values(void) CHECK(ZFS_IOC_BASE + 83 == ZFS_IOC_WAIT); CHECK(ZFS_IOC_BASE + 84 == ZFS_IOC_WAIT_FS); CHECK(ZFS_IOC_BASE + 87 == ZFS_IOC_POOL_SCRUB); + CHECK(ZFS_IOC_BASE + 91 == ZFS_IOC_SNAPSHOT_LIST_BATCH); CHECK(ZFS_IOC_PLATFORM_BASE + 1 == ZFS_IOC_EVENTS_NEXT); CHECK(ZFS_IOC_PLATFORM_BASE + 2 == ZFS_IOC_EVENTS_CLEAR); CHECK(ZFS_IOC_PLATFORM_BASE + 3 == ZFS_IOC_EVENTS_SEEK); diff --git a/tests/zfs-tests/cmd/snapshot_list_stats_test.c b/tests/zfs-tests/cmd/snapshot_list_stats_test.c new file mode 100644 index 000000000000..1480ff4fcad5 --- /dev/null +++ b/tests/zfs-tests/cmd/snapshot_list_stats_test.c @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: CDDL-1.0 +/* + * Verify projected snapshot-stat error handling against a real pool opened + * through libzpool. A link-time wrapper injects dmu_bonus_hold() errors for + * exact MOS objects, avoiding persistent pool damage and broad I/O faults. + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static __thread objset_t *fault_objset; +static __thread uint64_t fault_object; +static __thread boolean_t fault_reached; + +int __real_dmu_bonus_hold(objset_t *, uint64_t, const void *, dmu_buf_t **); +int __wrap_dmu_bonus_hold(objset_t *, uint64_t, const void *, dmu_buf_t **); + +/* + * Inject only the selected MOS object while preserving every other real read. + */ +int +__wrap_dmu_bonus_hold(objset_t *os, uint64_t object, const void *tag, + dmu_buf_t **dbp) +{ + if (os == fault_objset && fault_object != 0 && object == fault_object) { + fault_reached = B_TRUE; + return (EIO); + } + + return (__real_dmu_bonus_hold(os, object, tag, dbp)); +} + +static int +get_file_info(dmu_object_type_t bonus_type, const void *data, + zfs_file_info_t *zoi) +{ + (void) bonus_type; + (void) data; + (void) zoi; + return (ENOENT); +} + +static int +check_snapshot_stats(dsl_pool_t *dp, uint64_t dsobj, const char *description, + uint64_t injected_object, uint64_t min_txg, uint64_t max_txg, + int expected_error, boolean_t expected_valid, + boolean_t expected_injection) +{ + dsl_dataset_snapshot_stats_t stats; + boolean_t injected; + int error; + + fault_objset = dp->dp_meta_objset; + fault_object = injected_object; + fault_reached = B_FALSE; + error = dsl_dataset_snapshot_stats(dp, dsobj, B_FALSE, B_FALSE, + B_TRUE, min_txg, max_txg, &stats); + fault_objset = NULL; + fault_object = 0; + injected = fault_reached; + + if (error != expected_error || + stats.dss_written_valid != expected_valid || + injected != expected_injection) { + (void) fprintf(stderr, + "%s: error=%d written_valid=%u injected=%u; expected " + "error=%d written_valid=%u injected=%u\n", + description, error, stats.dss_written_valid, injected, + expected_error, expected_valid, expected_injection); + return (-1); + } + return (0); +} + +static int +import_pool_readonly(char *pool, const char *search_path) +{ + char *paths[] = { (char *)search_path }; + importargs_t args = { 0 }; + libpc_handle_t lpch = { + .lpc_lib_handle = NULL, + .lpc_ops = &libzpool_config_ops, + .lpc_printerr = B_TRUE + }; + nvlist_t *config = NULL; + nvlist_t *props = NULL; + int error; + + args.paths = 1; + args.path = paths; + args.can_be_active = B_TRUE; + error = zpool_find_config(&lpch, pool, &config, &args); + if (error != 0) + return (error); + + error = nvlist_alloc(&props, NV_UNIQUE_NAME, 0); + if (error == 0) { + error = nvlist_add_uint64(props, + zpool_prop_to_name(ZPOOL_PROP_READONLY), 1); + } + if (error == 0) { + error = spa_import(pool, config, props, ZFS_IMPORT_SKIP_MMP); + if (error == EEXIST) + error = 0; + } + nvlist_free(props); + nvlist_free(config); + return (error); +} + +int +main(int argc, char **argv) +{ + char pool[ZFS_MAX_DATASET_NAME_LEN]; + dsl_dataset_t *ds = NULL; + dsl_pool_t *dp = NULL; + dsl_dataset_phys_t *dsp; + uint64_t dsobj, deadlist_obj, prev_obj, creation_txg; + char *separator; + int error = 0; + + if (argc != 3) { + (void) fprintf(stderr, + "usage: %s snapshot search-path\n", argv[0]); + return (EXIT_FAILURE); + } + if (strlcpy(pool, argv[1], sizeof (pool)) >= sizeof (pool) || + (separator = strpbrk(pool, "/@")) == NULL) { + (void) fprintf(stderr, "invalid snapshot name: %s\n", argv[1]); + return (EXIT_FAILURE); + } + *separator = '\0'; + + zfs_prop_init(); + kernel_init(SPA_MODE_READ); + dmu_objset_register_type(DMU_OST_ZFS, get_file_info); + + error = import_pool_readonly(pool, argv[2]); + if (error != 0) { + (void) fprintf(stderr, "cannot import %s: %s\n", pool, + strerror(error)); + goto out; + } + error = dsl_pool_hold(argv[1], FTAG, &dp); + if (error != 0) { + (void) fprintf(stderr, "cannot hold %s: %s\n", pool, + strerror(error)); + goto out; + } + error = dsl_dataset_hold(dp, argv[1], FTAG, &ds); + if (error != 0) { + (void) fprintf(stderr, "cannot hold %s: %s\n", argv[1], + strerror(error)); + goto out; + } + + dsp = dsl_dataset_phys(ds); + dsobj = ds->ds_object; + deadlist_obj = dsp->ds_deadlist_obj; + prev_obj = dsp->ds_prev_snap_obj; + creation_txg = dsp->ds_creation_txg; + dsl_dataset_rele(ds, FTAG); + ds = NULL; + if (deadlist_obj == 0 || prev_obj == 0) { + (void) fprintf(stderr, "%s lacks a deadlist or predecessor\n", + argv[1]); + error = EINVAL; + goto out; + } + + if (check_snapshot_stats(dp, dsobj, "normal written lookup", 0, 0, 0, + 0, B_TRUE, B_FALSE) != 0 || + check_snapshot_stats(dp, dsobj, "exact-TXG written lookup", 0, + creation_txg, creation_txg, 0, B_TRUE, B_FALSE) != 0 || + check_snapshot_stats(dp, dsobj, "current deadlist EIO", + deadlist_obj, 0, 0, EIO, B_FALSE, B_TRUE) != 0 || + check_snapshot_stats(dp, dsobj, "predecessor EIO", prev_obj, 0, 0, + 0, B_FALSE, B_TRUE) != 0 || + check_snapshot_stats(dp, dsobj, + "below-minimum current deadlist EIO", deadlist_obj, + creation_txg + 1, 0, EIO, B_FALSE, B_TRUE) != 0 || + check_snapshot_stats(dp, dsobj, "below-minimum predecessor EIO", + prev_obj, creation_txg + 1, 0, 0, B_FALSE, B_FALSE) != 0 || + check_snapshot_stats(dp, dsobj, + "above-maximum current deadlist EIO", deadlist_obj, 0, + creation_txg - 1, EIO, B_FALSE, B_TRUE) != 0 || + check_snapshot_stats(dp, dsobj, "above-maximum predecessor EIO", + prev_obj, 0, creation_txg - 1, 0, B_FALSE, B_FALSE) != 0) { + error = EPROTO; + } + +out: + if (ds != NULL) + dsl_dataset_rele(ds, FTAG); + if (dp != NULL) + dsl_pool_rele(dp, FTAG); + kernel_fini(); + return (error == 0 ? EXIT_SUCCESS : EXIT_FAILURE); +} diff --git a/tests/zfs-tests/cmd/snapshot_list_test.c b/tests/zfs-tests/cmd/snapshot_list_test.c new file mode 100644 index 000000000000..d835d2350467 --- /dev/null +++ b/tests/zfs-tests/cmd/snapshot_list_test.c @@ -0,0 +1,835 @@ +// SPDX-License-Identifier: CDDL-1.0 +/* + * Exercise projected snapshot iteration behavior which is not exposed by the + * zfs command line. + */ + +#include +#include +#include +#include +#include +#include + +#include + +static int +close_snapshot(zfs_handle_t *zhp, void *arg __attribute__((unused))) +{ + zfs_close(zhp); + return (0); +} + +static int +count_snapshot(zfs_handle_t *zhp, void *arg) +{ + unsigned int *count = arg; + + (*count)++; + zfs_close(zhp); + return (0); +} + +static int +print_snapshot(zfs_handle_t *zhp, void *arg __attribute__((unused))) +{ + int error = 0; + + if (puts(zfs_get_name(zhp)) == EOF) + error = EIO; + zfs_close(zhp); + return (error); +} + +typedef struct callback_error_arg { + int cea_error; + unsigned int cea_calls; +} callback_error_arg_t; + +typedef struct snapshot_metadata_arg { + zfs_type_t sma_underlying_type; + boolean_t sma_encrypted; + unsigned int sma_calls; +} snapshot_metadata_arg_t; + +static int +return_callback_error(zfs_handle_t *zhp, void *arg) +{ + callback_error_arg_t *callback = arg; + + zfs_close(zhp); + callback->cea_calls++; + return (callback->cea_calls >= 2 ? callback->cea_error : 0); +} + +static int +collect_snapshot_metadata(zfs_handle_t *zhp, void *arg) +{ + snapshot_metadata_arg_t *metadata = arg; + + metadata->sma_underlying_type = zfs_get_underlying_type(zhp); + metadata->sma_encrypted = zfs_is_encrypted(zhp); + metadata->sma_calls++; + zfs_close(zhp); + return (0); +} + +static int +check_encrypted_snapshot_metadata(zfs_handle_t *zhp, void *arg) +{ + unsigned int *callbacks = arg; + zfs_type_t underlying_type = zfs_get_underlying_type(zhp); + boolean_t encrypted = zfs_is_encrypted(zhp); + int error = 0; + + (*callbacks)++; + if (underlying_type != ZFS_TYPE_FILESYSTEM || encrypted != B_TRUE) { + (void) fprintf(stderr, + "expected %s type/encryption %d/%d, got %d/%d\n", + zfs_get_name(zhp), ZFS_TYPE_FILESYSTEM, B_TRUE, + underlying_type, encrypted); + error = EPROTO; + } + zfs_close(zhp); + return (error); +} + +static uint64_t +parse_uint64(const char *value) +{ + char *end; + uint64_t result; + + errno = 0; + result = strtoull(value, &end, 10); + if (errno != 0 || value[0] == '\0' || end[0] != '\0') { + (void) fprintf(stderr, "invalid integer: %s\n", value); + exit(EXIT_FAILURE); + } + return (result); +} + +static int +open_dataset(libzfs_handle_t **hdl, zfs_handle_t **zhp, const char *name) +{ + *hdl = libzfs_init(); + if (*hdl == NULL) { + (void) fprintf(stderr, "libzfs_init failed\n"); + return (EXIT_FAILURE); + } + + *zhp = zfs_open(*hdl, name, ZFS_TYPE_FILESYSTEM | ZFS_TYPE_VOLUME); + if (*zhp == NULL) { + (void) fprintf(stderr, "cannot open %s\n", name); + libzfs_fini(*hdl); + return (EXIT_FAILURE); + } + return (EXIT_SUCCESS); +} + +static int +run_encrypted_snapshot_metadata(const char *name) +{ + libzfs_handle_t *hdl; + zfs_handle_t *zhp; + unsigned int callbacks = 0; + int error; + + if (open_dataset(&hdl, &zhp, name) != 0) + return (EXIT_FAILURE); + + error = zfs_iter_snapshots_v2(zhp, ZFS_ITER_BATCHED, + check_encrypted_snapshot_metadata, &callbacks, 0, 0); + zfs_close(zhp); + libzfs_fini(hdl); + if (error != 0 || callbacks == 0) { + (void) fprintf(stderr, + "expected encrypted projected snapshot callbacks, " + "got error %d and %u callbacks\n", error, callbacks); + return (EXIT_FAILURE); + } + return (EXIT_SUCCESS); +} + +static int +run_filter(const char *name, const char *minimum, const char *maximum) +{ + libzfs_handle_t *hdl; + zfs_handle_t *zhp; + int error; + + if (open_dataset(&hdl, &zhp, name) != 0) + return (EXIT_FAILURE); + + error = zfs_iter_snapshots_v2(zhp, ZFS_ITER_BATCHED | + ZFS_ITER_BATCHED_CREATETXG | ZFS_ITER_BATCHED_GUID | + ZFS_ITER_BATCHED_WRITTEN, + print_snapshot, NULL, parse_uint64(minimum), parse_uint64(maximum)); + zfs_close(zhp); + libzfs_fini(hdl); + if (error != 0) { + (void) fprintf(stderr, "snapshot iteration failed: %s\n", + strerror(error)); + return (EXIT_FAILURE); + } + return (EXIT_SUCCESS); +} + +static int +run_sorted(const char *name) +{ + libzfs_handle_t *hdl; + zfs_handle_t *zhp; + int error; + + if (open_dataset(&hdl, &zhp, name) != 0) + return (EXIT_FAILURE); + + error = zfs_iter_snapshots_sorted_v2(zhp, ZFS_ITER_BATCHED, + print_snapshot, NULL, 0, 0); + zfs_close(zhp); + libzfs_fini(hdl); + if (error != 0) { + (void) fprintf(stderr, "sorted snapshot iteration failed: %s\n", + strerror(error)); + return (EXIT_FAILURE); + } + return (EXIT_SUCCESS); +} + +static int +run_dependents(const char *name, boolean_t simple) +{ + libzfs_handle_t *hdl; + zfs_handle_t *zhp; + int error, flags = ZFS_ITER_BATCHED; + + if (open_dataset(&hdl, &zhp, name) != 0) + return (EXIT_FAILURE); + + if (simple) + flags |= ZFS_ITER_SIMPLE; + error = zfs_iter_dependents_v2(zhp, flags, B_FALSE, + print_snapshot, NULL); + zfs_close(zhp); + libzfs_fini(hdl); + if (error != 0) { + (void) fprintf(stderr, "dependent iteration failed: %s\n", + strerror(error)); + return (EXIT_FAILURE); + } + return (EXIT_SUCCESS); +} + +static int +run_interrupt(const char *name) +{ + libzfs_handle_t *hdl; + zfs_handle_t *zhp; + int error, iteration_errno; + + if (open_dataset(&hdl, &zhp, name) != 0) + return (EXIT_FAILURE); + + errno = 0; + error = zfs_iter_snapshots_v2(zhp, ZFS_ITER_BATCHED | + ZFS_ITER_BATCHED_CREATETXG | ZFS_ITER_BATCHED_GUID | + ZFS_ITER_BATCHED_CREATION | ZFS_ITER_BATCHED_USERREFS, + close_snapshot, NULL, 0, 0); + iteration_errno = errno; + + zfs_close(zhp); + libzfs_fini(hdl); + if (error != -1 || iteration_errno != EINTR) { + (void) fprintf(stderr, "expected -1/EINTR, got %d/%d (%s)\n", + error, iteration_errno, error == 0 ? "success" : + strerror(iteration_errno)); + return (EXIT_FAILURE); + } + return (EXIT_SUCCESS); +} + +static int +run_partial_error(const char *name, const char *mode, + const char *expected_value) +{ + libzfs_handle_t *hdl; + zfs_handle_t *zhp; + uint64_t expected_callbacks = parse_uint64(expected_value); + unsigned int callbacks = 0; + int flags, error, iteration_errno, libzfs_error; + + if (strcmp(mode, "legacy") == 0) { + flags = 0; + } else if (strcmp(mode, "batched") == 0) { + flags = ZFS_ITER_BATCHED | ZFS_ITER_BATCHED_USERREFS; + } else { + (void) fprintf(stderr, "unknown iterator mode: %s\n", mode); + return (EXIT_FAILURE); + } + if (open_dataset(&hdl, &zhp, name) != 0) + return (EXIT_FAILURE); + + libzfs_print_on_error(hdl, B_FALSE); + errno = 0; + error = zfs_iter_snapshots_v2(zhp, flags, count_snapshot, &callbacks, + 0, 0); + iteration_errno = errno; + libzfs_error = libzfs_errno(hdl); + + zfs_close(zhp); + libzfs_fini(hdl); + if (error != -1 || iteration_errno != EIO || + libzfs_error != EZFS_IO || callbacks != expected_callbacks) { + (void) fprintf(stderr, + "expected %s -1/%d/%d/%" PRIu64 ", got %d/%d/%d/%u\n", + mode, EIO, EZFS_IO, expected_callbacks, error, + iteration_errno, libzfs_error, callbacks); + return (EXIT_FAILURE); + } + return (EXIT_SUCCESS); +} + +static int +materialize_snapshot(zfs_handle_t *zhp, void *arg) +{ + unsigned int *callbacks = arg; + nvlist_t *props = zfs_get_all_props(zhp); + int materialize_errno; + + if (props == NULL) { + materialize_errno = errno; + props = zfs_get_all_props(zhp); + if (props != NULL && + nvlist_exists(props, zfs_prop_to_name(ZFS_PROP_CREATION)) && + nvlist_exists(props, zfs_prop_to_name(ZFS_PROP_USERREFS))) { + (*callbacks)++; + } + zfs_close(zhp); + errno = materialize_errno; + return (-1); + } + zfs_close(zhp); + (*callbacks)++; + return (0); +} + +static int +run_handle_enomem(const char *name) +{ + libzfs_handle_t *hdl; + zfs_handle_t *zhp; + unsigned int callbacks = 0; + int error, iteration_errno, libzfs_error; + + if (open_dataset(&hdl, &zhp, name) != 0) + return (EXIT_FAILURE); + + libzfs_print_on_error(hdl, B_FALSE); + errno = 0; + error = zfs_iter_snapshots_v2(zhp, ZFS_ITER_BATCHED | + ZFS_ITER_BATCHED_CREATION | ZFS_ITER_BATCHED_USERREFS, + materialize_snapshot, &callbacks, 0, 0); + iteration_errno = errno; + libzfs_error = libzfs_errno(hdl); + + zfs_close(zhp); + libzfs_fini(hdl); + if (error != -1 || iteration_errno != ENOMEM || + libzfs_error != EZFS_NOMEM || callbacks != 1) { + (void) fprintf(stderr, "expected -1/%d/%d/1, got %d/%d/%d/%u\n", + ENOMEM, EZFS_NOMEM, error, iteration_errno, libzfs_error, + callbacks); + return (EXIT_FAILURE); + } + return (EXIT_SUCCESS); +} + +static int +check_property_getters(zfs_handle_t *zhp) +{ + char creation[ZFS_MAXPROPLEN], userrefs[ZFS_MAXPROPLEN]; + uint64_t creation_value, userrefs_value; + + creation_value = zfs_prop_get_int(zhp, ZFS_PROP_CREATION); + userrefs_value = zfs_prop_get_int(zhp, ZFS_PROP_USERREFS); + if (zfs_prop_get(zhp, ZFS_PROP_CREATION, creation, + sizeof (creation), NULL, NULL, 0, B_TRUE) != 0 || + zfs_prop_get(zhp, ZFS_PROP_USERREFS, userrefs, + sizeof (userrefs), NULL, NULL, 0, B_TRUE) != 0 || + parse_uint64(creation) != creation_value || + parse_uint64(userrefs) != userrefs_value) { + return (EPROTO); + } + return (0); +} + +static int +check_direct_properties(zfs_handle_t *zhp, void *arg) +{ + unsigned int *callbacks = arg; + zfs_handle_t *copy = zfs_handle_dup(zhp); + int error; + + zfs_close(zhp); + if (copy == NULL) + return (ENOMEM); + error = check_property_getters(copy); + zfs_close(copy); + if (error != 0) + return (error); + + (*callbacks)++; + return (0); +} + +static int +check_pruned_property(zfs_handle_t *zhp, zfs_prop_t pruned_prop, + zfs_prop_t retained_prop) +{ + zfs_handle_t *copy = zfs_handle_dup(zhp); + uint8_t props_table[ZFS_NUM_PROPS]; + nvlist_t *props; + int error = 0; + + if (copy == NULL) + return (ENOMEM); + (void) memset(props_table, B_TRUE, sizeof (props_table)); + props_table[pruned_prop] = B_FALSE; + zfs_prune_proplist(copy, props_table); + props = zfs_get_all_props(copy); + if (props == NULL || + nvlist_exists(props, zfs_prop_to_name(pruned_prop)) || + !nvlist_exists(props, zfs_prop_to_name(retained_prop))) { + error = EPROTO; + } + zfs_close(copy); + return (error); +} + +static int +check_materialized_properties(zfs_handle_t *zhp, void *arg) +{ + unsigned int *callbacks = arg; + nvlist_t *props, *creation_prop, *userrefs_prop; + uintptr_t clones; + uint64_t creation, userrefs; + int error = check_property_getters(zhp); + + if (error != 0) + goto out; + error = check_pruned_property(zhp, ZFS_PROP_CREATION, + ZFS_PROP_USERREFS); + if (error != 0) + goto out; + error = check_pruned_property(zhp, ZFS_PROP_USERREFS, + ZFS_PROP_CREATION); + if (error != 0) + goto out; + + clones = (uintptr_t)zfs_get_clones_nvl(zhp); + if (clones == 0) { + error = EPROTO; + goto out; + } + props = zfs_get_all_props(zhp); + if (props == NULL || + nvlist_lookup_nvlist(props, zfs_prop_to_name(ZFS_PROP_CREATION), + &creation_prop) != 0 || + nvlist_lookup_nvlist(props, zfs_prop_to_name(ZFS_PROP_USERREFS), + &userrefs_prop) != 0 || + nvlist_lookup_uint64(creation_prop, ZPROP_VALUE, &creation) != 0 || + nvlist_lookup_uint64(userrefs_prop, ZPROP_VALUE, &userrefs) != 0 || + creation != zfs_prop_get_int(zhp, ZFS_PROP_CREATION) || + userrefs != zfs_prop_get_int(zhp, ZFS_PROP_USERREFS) || + clones != (uintptr_t)zfs_get_clones_nvl(zhp)) { + error = EPROTO; + } + +out: + zfs_close(zhp); + if (error == 0) + (*callbacks)++; + return (error); +} + +static int +check_refreshed_properties(zfs_handle_t *zhp, void *arg) +{ + zfs_refresh_properties(zhp); + return (check_materialized_properties(zhp, arg)); +} + +static int +run_projected_properties(const char *name, boolean_t materialize, + boolean_t refresh) +{ + libzfs_handle_t *hdl; + zfs_handle_t *zhp; + unsigned int callbacks = 0; + int error; + + if (open_dataset(&hdl, &zhp, name) != 0) + return (EXIT_FAILURE); + + error = zfs_iter_snapshots_v2(zhp, ZFS_ITER_BATCHED | + ZFS_ITER_BATCHED_CREATION | ZFS_ITER_BATCHED_USERREFS, + refresh ? check_refreshed_properties : + (materialize ? check_materialized_properties : + check_direct_properties), &callbacks, 0, 0); + + zfs_close(zhp); + libzfs_fini(hdl); + if (error != 0 || callbacks == 0) { + (void) fprintf(stderr, "expected successful projected " + "properties, got error/callbacks %d/%u\n", + error, callbacks); + return (EXIT_FAILURE); + } + return (EXIT_SUCCESS); +} + +static int +run_metadata_eproto(const char *name) +{ + libzfs_handle_t *hdl; + zfs_handle_t *zhp; + unsigned int callbacks = 0; + int error, iteration_errno; + + if (open_dataset(&hdl, &zhp, name) != 0) + return (EXIT_FAILURE); + + libzfs_print_on_error(hdl, B_FALSE); + errno = 0; + error = zfs_iter_snapshots_v2(zhp, + ZFS_ITER_BATCHED | ZFS_ITER_BATCHED_WRITTEN, + count_snapshot, &callbacks, 0, 0); + iteration_errno = errno; + + zfs_close(zhp); + libzfs_fini(hdl); + if (error != -1 || iteration_errno != EPROTO || callbacks != 0) { + (void) fprintf(stderr, "expected -1/%d/0, got %d/%d/%u\n", + EPROTO, error, iteration_errno, callbacks); + return (EXIT_FAILURE); + } + return (EXIT_SUCCESS); +} + +static int +run_callback_error(const char *name, const char *error_name, + boolean_t bookmarks) +{ + libzfs_handle_t *hdl; + zfs_handle_t *zhp; + callback_error_arg_t callback = { 0 }; + int error; + + if (strcmp(error_name, "cmd_unavail") == 0) + callback.cea_error = ZFS_ERR_IOC_CMD_UNAVAIL; + else if (strcmp(error_name, "arg_unavail") == 0) + callback.cea_error = ZFS_ERR_IOC_ARG_UNAVAIL; + else if (strcmp(error_name, "enotty") == 0) + callback.cea_error = ENOTTY; + else if (strcmp(error_name, "enotsup") == 0) + callback.cea_error = ENOTSUP; + else if (strcmp(error_name, "enoent") == 0) + callback.cea_error = ENOENT; + else if (strcmp(error_name, "esrch") == 0) + callback.cea_error = ESRCH; + else { + (void) fprintf(stderr, "unknown callback error: %s\n", + error_name); + return (EXIT_FAILURE); + } + + if (open_dataset(&hdl, &zhp, name) != 0) + return (EXIT_FAILURE); + + if (bookmarks) { + error = zfs_iter_bookmarks_v2(zhp, ZFS_ITER_BATCHED, + return_callback_error, &callback); + } else { + error = zfs_iter_snapshots_v2(zhp, ZFS_ITER_BATCHED, + return_callback_error, &callback, 0, 0); + } + zfs_close(zhp); + libzfs_fini(hdl); + if (error != callback.cea_error || callback.cea_calls != 2) { + (void) fprintf(stderr, "expected callback result/calls %d/2, " + "got %d/%u\n", callback.cea_error, error, + callback.cea_calls); + return (EXIT_FAILURE); + } + return (EXIT_SUCCESS); +} + +static int +run_bookmark_error(const char *name, const char *error_name) +{ + libzfs_handle_t *hdl; + zfs_handle_t *zhp; + unsigned int callbacks = 0; + int expected, wrapper_error, legacy_error, batched_error; + + if (strcmp(error_name, "eio") == 0) + expected = EIO; + else if (strcmp(error_name, "enoent") == 0) + expected = ENOENT; + else if (strcmp(error_name, "esrch") == 0) + expected = ESRCH; + else { + (void) fprintf(stderr, "unknown bookmark error: %s\n", + error_name); + return (EXIT_FAILURE); + } + + if (open_dataset(&hdl, &zhp, name) != 0) + return (EXIT_FAILURE); + + wrapper_error = zfs_iter_bookmarks(zhp, count_snapshot, &callbacks); + legacy_error = zfs_iter_bookmarks_v2(zhp, 0, count_snapshot, + &callbacks); + batched_error = zfs_iter_bookmarks_v2(zhp, ZFS_ITER_BATCHED, + count_snapshot, &callbacks); + zfs_close(zhp); + libzfs_fini(hdl); + + if (wrapper_error != expected || legacy_error != expected || + batched_error != expected || callbacks != 0) { + (void) fprintf(stderr, "expected bookmark errors/callbacks " + "%d/%d/%d/0, got %d/%d/%d/%u\n", expected, expected, + expected, wrapper_error, legacy_error, batched_error, + callbacks); + return (EXIT_FAILURE); + } + return (EXIT_SUCCESS); +} + +static int +run_stale_bookmarks(const char *name, const char *target) +{ + libzfs_handle_t *hdl; + zfs_handle_t *stale, *mutator = NULL; + unsigned int callbacks = 0; + const char *operation = target == NULL ? "destroy" : "rename"; + int error, wrapper_error, legacy_error, batched_error; + int result = EXIT_FAILURE; + + if (open_dataset(&hdl, &stale, name) != 0) + return (EXIT_FAILURE); + + mutator = zfs_open(hdl, name, ZFS_TYPE_FILESYSTEM | ZFS_TYPE_VOLUME); + if (mutator == NULL) { + (void) fprintf(stderr, "cannot open second handle for %s\n", + name); + goto out; + } + + if (target == NULL) { + error = zfs_destroy(mutator, B_FALSE); + } else { + renameflags_t flags = { .nounmount = B_TRUE }; + + error = zfs_rename(mutator, target, flags); + } + if (error != 0) { + (void) fprintf(stderr, "cannot %s %s: %s\n", operation, name, + libzfs_error_description(hdl)); + goto out; + } + zfs_close(mutator); + mutator = NULL; + + wrapper_error = zfs_iter_bookmarks(stale, count_snapshot, &callbacks); + legacy_error = zfs_iter_bookmarks_v2(stale, 0, count_snapshot, + &callbacks); + batched_error = zfs_iter_bookmarks_v2(stale, ZFS_ITER_BATCHED, + count_snapshot, &callbacks); + if (wrapper_error != ENOENT || legacy_error != ENOENT || + batched_error != ENOENT || callbacks != 0) { + (void) fprintf(stderr, + "expected stale bookmark errors/callbacks " + "after %s to be %d/%d/%d/0, got %d/%d/%d/%u\n", + operation, ENOENT, ENOENT, ENOENT, wrapper_error, + legacy_error, batched_error, callbacks); + goto out; + } + result = EXIT_SUCCESS; + +out: + if (mutator != NULL) + zfs_close(mutator); + zfs_close(stale); + libzfs_fini(hdl); + return (result); +} + +static int +run_stale_snapshot_metadata(const char *name, const char *type_name) +{ + libzfs_handle_t *hdl; + zfs_handle_t *stale, *mutator = NULL; + nvlist_t *props = NULL; + snapshot_metadata_arg_t batched = { 0 }, legacy = { 0 }; + zfs_type_t expected_type; + char snapname[ZFS_MAX_DATASET_NAME_LEN]; + int batched_error, legacy_error, result = EXIT_FAILURE; + + if (strcmp(type_name, "filesystem") == 0) + expected_type = ZFS_TYPE_FILESYSTEM; + else if (strcmp(type_name, "volume") == 0) + expected_type = ZFS_TYPE_VOLUME; + else { + (void) fprintf(stderr, "unknown replacement type: %s\n", + type_name); + return (EXIT_FAILURE); + } + if (open_dataset(&hdl, &stale, name) != 0) + return (EXIT_FAILURE); + if (zfs_get_underlying_type(stale) == expected_type && + !zfs_is_encrypted(stale)) { + (void) fprintf(stderr, "%s does not exercise stale metadata\n", + name); + goto out; + } + + mutator = zfs_open(hdl, name, ZFS_TYPE_FILESYSTEM | ZFS_TYPE_VOLUME); + if (mutator == NULL) { + (void) fprintf(stderr, "cannot open second handle for %s\n", + name); + goto out; + } + if (zfs_destroy(mutator, B_FALSE) != 0) { + (void) fprintf(stderr, "cannot destroy %s: %s\n", name, + libzfs_error_description(hdl)); + goto out; + } + zfs_close(mutator); + mutator = NULL; + + if (nvlist_alloc(&props, NV_UNIQUE_NAME, 0) != 0) { + (void) fprintf(stderr, + "cannot allocate replacement properties\n"); + goto out; + } + if (expected_type == ZFS_TYPE_VOLUME && + nvlist_add_uint64(props, zfs_prop_to_name(ZFS_PROP_VOLSIZE), + 64 * 1024 * 1024) != 0) { + (void) fprintf(stderr, "cannot set replacement volume size\n"); + goto out; + } + if (zfs_create(hdl, name, expected_type, props) != 0) { + (void) fprintf(stderr, "cannot recreate %s: %s\n", name, + libzfs_error_description(hdl)); + goto out; + } + if (snprintf(snapname, sizeof (snapname), "%s@replacement", name) >= + sizeof (snapname)) { + (void) fprintf(stderr, "snapshot name is too long: %s\n", name); + goto out; + } + if (zfs_snapshot(hdl, snapname, B_FALSE, NULL) != 0) { + (void) fprintf(stderr, "cannot snapshot %s: %s\n", name, + libzfs_error_description(hdl)); + goto out; + } + + batched_error = zfs_iter_snapshots_v2(stale, ZFS_ITER_BATCHED, + collect_snapshot_metadata, &batched, 0, 0); + legacy_error = zfs_iter_snapshots_v2(stale, 0, + collect_snapshot_metadata, &legacy, 0, 0); + if (batched_error != 0 || legacy_error != 0 || batched.sma_calls != 1 || + legacy.sma_calls != 1 || + batched.sma_underlying_type != expected_type || + legacy.sma_underlying_type != expected_type || + batched.sma_encrypted || legacy.sma_encrypted) { + (void) fprintf(stderr, + "expected errors/calls/types/encryption " + "0/0/1/1/%d/%d/%d/%d, got " + "%d/%d/%u/%u/%d/%d/%d/%d\n", + expected_type, expected_type, B_FALSE, B_FALSE, + batched_error, legacy_error, + batched.sma_calls, legacy.sma_calls, + batched.sma_underlying_type, legacy.sma_underlying_type, + batched.sma_encrypted, legacy.sma_encrypted); + goto out; + } + result = EXIT_SUCCESS; + +out: + nvlist_free(props); + if (mutator != NULL) + zfs_close(mutator); + zfs_close(stale); + libzfs_fini(hdl); + return (result); +} + +int +main(int argc, char **argv) +{ + if (argc == 5 && strcmp(argv[1], "filter") == 0) + return (run_filter(argv[2], argv[3], argv[4])); + if (argc == 3 && strcmp(argv[1], "sorted") == 0) + return (run_sorted(argv[2])); + if (argc == 3 && strcmp(argv[1], "dependents") == 0) + return (run_dependents(argv[2], B_FALSE)); + if (argc == 3 && strcmp(argv[1], "dependents-simple") == 0) + return (run_dependents(argv[2], B_TRUE)); + if (argc == 3 && strcmp(argv[1], "interrupt") == 0) + return (run_interrupt(argv[2])); + if (argc == 5 && strcmp(argv[1], "partial-error") == 0) + return (run_partial_error(argv[2], argv[3], argv[4])); + if (argc == 3 && strcmp(argv[1], "handle-enomem") == 0) + return (run_handle_enomem(argv[2])); + if (argc == 3 && strcmp(argv[1], "direct-properties") == 0) + return (run_projected_properties(argv[2], B_FALSE, B_FALSE)); + if (argc == 3 && strcmp(argv[1], "materialized-properties") == 0) + return (run_projected_properties(argv[2], B_TRUE, B_FALSE)); + if (argc == 3 && strcmp(argv[1], "refreshed-properties") == 0) + return (run_projected_properties(argv[2], B_FALSE, B_TRUE)); + if (argc == 3 && strcmp(argv[1], "metadata-eproto") == 0) + return (run_metadata_eproto(argv[2])); + if (argc == 4 && strcmp(argv[1], "callback-error") == 0) + return (run_callback_error(argv[2], argv[3], B_FALSE)); + if (argc == 4 && strcmp(argv[1], "bookmark-callback-error") == 0) + return (run_callback_error(argv[2], argv[3], B_TRUE)); + if (argc == 4 && strcmp(argv[1], "bookmark-error") == 0) + return (run_bookmark_error(argv[2], argv[3])); + if (argc == 3 && strcmp(argv[1], "stale-bookmarks-destroy") == 0) + return (run_stale_bookmarks(argv[2], NULL)); + if (argc == 4 && strcmp(argv[1], "stale-bookmarks-rename") == 0) + return (run_stale_bookmarks(argv[2], argv[3])); + if (argc == 4 && strcmp(argv[1], "stale-snapshot-metadata") == 0) + return (run_stale_snapshot_metadata(argv[2], argv[3])); + if (argc == 3 && + strcmp(argv[1], "encrypted-snapshot-metadata") == 0) + return (run_encrypted_snapshot_metadata(argv[2])); + + (void) fprintf(stderr, "usage: %s filter dataset min_txg max_txg\n" + " %s encrypted-snapshot-metadata dataset\n" + " %s sorted dataset\n" + " %s dependents dataset\n" + " %s dependents-simple dataset\n" + " %s interrupt dataset\n" + " %s partial-error dataset mode callbacks\n" + " %s handle-enomem dataset\n" + " %s direct-properties dataset\n" + " %s materialized-properties dataset\n" + " %s refreshed-properties dataset\n" + " %s metadata-eproto dataset\n" + " %s callback-error dataset error\n" + " %s bookmark-callback-error dataset error\n" + " %s bookmark-error dataset error\n" + " %s stale-bookmarks-destroy dataset\n" + " %s stale-bookmarks-rename dataset target\n" + " %s stale-snapshot-metadata dataset type\n", + argv[0], argv[0], + argv[0], argv[0], argv[0], argv[0], argv[0], argv[0], argv[0], + argv[0], + argv[0], argv[0], argv[0], argv[0], argv[0], argv[0], argv[0], + argv[0]); + return (EXIT_FAILURE); +} diff --git a/tests/zfs-tests/include/commands.cfg b/tests/zfs-tests/include/commands.cfg index 7bd1ac915625..4985b3b2cdf7 100644 --- a/tests/zfs-tests/include/commands.cfg +++ b/tests/zfs-tests/include/commands.cfg @@ -228,6 +228,8 @@ export ZFSTEST_FILES_COMMON='badsend rename_dir rm_lnkcnt_zero_file send_doall + snapshot_list_test + snapshot_list_stats_test statx threadsappend user_ns_exec diff --git a/tests/zfs-tests/include/tunables.cfg b/tests/zfs-tests/include/tunables.cfg index 14c49c0b6a8e..cd996761f55c 100644 --- a/tests/zfs-tests/include/tunables.cfg +++ b/tests/zfs-tests/include/tunables.cfg @@ -91,6 +91,8 @@ SCAN_VDEV_LIMIT scan_vdev_limit zfs_scan_vdev_limit SCRUB_AFTER_EXPAND scrub_after_expand zfs_scrub_after_expand SEND_HOLES_WITHOUT_BIRTH_TIME send_holes_without_birth_time send_holes_without_birth_time SLOW_IO_EVENTS_PER_SECOND slow_io_events_per_second zfs_slow_io_events_per_second +SNAPSHOT_LIST_BATCH_SIZE snapshot_list_batch_size zfs_snapshot_list_batch_size +SNAPSHOT_LIST_BATCH_TIME_US snapshot_list_batch_time_us zfs_snapshot_list_batch_time_us SNAPSHOT_NO_SETUID UNSUPPORTED zfs_snapshot_no_setuid SPA_ASIZE_INFLATION spa.asize_inflation spa_asize_inflation SPA_DISCARD_MEMORY_LIMIT spa.discard_memory_limit zfs_spa_discard_memory_limit diff --git a/tests/zfs-tests/tests/Makefile.am b/tests/zfs-tests/tests/Makefile.am index a7c0881f2d81..8004ee10b0b2 100644 --- a/tests/zfs-tests/tests/Makefile.am +++ b/tests/zfs-tests/tests/Makefile.am @@ -828,6 +828,13 @@ nobase_dist_datadir_zfs_tests_tests_SCRIPTS += \ functional/cli_root/zfs_list/cleanup.ksh \ functional/cli_root/zfs_list/setup.ksh \ functional/cli_root/zfs_list/zfs_list_009_pos.ksh \ + functional/cli_root/zfs_list/zfs_list_010_pos.ksh \ + functional/cli_root/zfs_list/zfs_list_011_pos.ksh \ + functional/cli_root/zfs_list/zfs_list_012_pos.ksh \ + functional/cli_root/zfs_list/zfs_list_013_pos.ksh \ + functional/cli_root/zfs_list/zfs_list_014_pos.ksh \ + functional/cli_root/zfs_list/zfs_list_016_pos.ksh \ + functional/cli_root/zfs_list/zfs_list_017_pos.ksh \ functional/cli_root/zfs_load-key/cleanup.ksh \ functional/cli_root/zfs_load-key/setup.ksh \ functional/cli_root/zfs_load-key/zfs_load-key_all.ksh \ diff --git a/tests/zfs-tests/tests/functional/cli_root/zfs_list/zfs_list_010_pos.ksh b/tests/zfs-tests/tests/functional/cli_root/zfs_list/zfs_list_010_pos.ksh new file mode 100755 index 000000000000..c313e2b5940d --- /dev/null +++ b/tests/zfs-tests/tests/functional/cli_root/zfs_list/zfs_list_010_pos.ksh @@ -0,0 +1,350 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# + +. $STF_SUITE/include/libtest.shlib + +# +# DESCRIPTION: +# Batched snapshot and bookmark listing honors its tunable bounds and handles +# projected objects across operational dataset states. +# +# STRATEGY: +# 1. Verify the tunable bounds, then isolate the minimum time budget from the +# batch-size limit and exercise cursor progress at the minimum batch size. +# 2. Verify JSON preserves hidden creation-TXG metadata and integer fields. +# 3. Verify bookmark projection after its source snapshot has been destroyed. +# 4. Verify snapshots remain listable after an encrypted dataset's key unloads. +# 5. Verify recursive mixed-type output combines simple, projected snapshot, +# and projected bookmark handles without omissions, duplicates, or changed +# ordering. +# 6. Verify a held, deferred-destroy snapshot remains projected with userrefs. +# 7. Verify listsnapshots=on implicitly includes projected snapshots. +# 8. Verify a combined snapshot and bookmark list when only a bookmark exists. +# 9. Verify nonrecursive combined-type listing stays on each explicitly named +# dataset and does not broaden mixed-type lists. +# + +verify_runnable "global" +set -o pipefail + +DATASET="$TESTPOOL/$TESTFS/projected_list" +ENCRYPTED_DATASET="$TESTPOOL/$TESTFS/projected_list_encrypted" +MIXED_DATASET="$TESTPOOL/$TESTFS/projected_list_mixed" +MIXED_CHILD="$MIXED_DATASET/child" +MIXED_VOLUME="$MIXED_DATASET/vol" +DATASET_MOUNT="$TESTDIR/projected_list" +BATCH_OUTPUT="$TEST_BASE_DIR/projected_list_batch.$$" +LEGACY_OUTPUT="$TEST_BASE_DIR/projected_list_legacy.$$" +EXPECTED_OUTPUT="$TEST_BASE_DIR/projected_list_expected.$$" +MARKER="$TEST_BASE_DIR/projected_list_marker.$$" +COLUMNS="createtxg,creation,guid,name,written,type,userrefs,objsetid" +HOLD_TAG="projected-list" +DEFERRED_HOLD_TAG="projected-list-deferred" +ENCRYPTED_HOLD_TAG="projected-list-encrypted" +saved_listsnapshots="" + +function cleanup +{ + zfs release "$HOLD_TAG" "$DATASET@after_append" >/dev/null 2>&1 + zfs release "$DEFERRED_HOLD_TAG" "$DATASET@deferred" \ + >/dev/null 2>&1 + zfs release "$ENCRYPTED_HOLD_TAG" "$ENCRYPTED_DATASET@second" \ + >/dev/null 2>&1 + [[ -n "$saved_listsnapshots" ]] && zpool set \ + listsnapshots="$saved_listsnapshots" "$TESTPOOL" >/dev/null 2>&1 + rm -f "$BATCH_OUTPUT" "$LEGACY_OUTPUT" "$EXPECTED_OUTPUT" "$MARKER" + datasetexists "$MIXED_DATASET" && zfs destroy -r "$MIXED_DATASET" + datasetexists "$ENCRYPTED_DATASET" && \ + zfs destroy -r "$ENCRYPTED_DATASET" + datasetexists "$DATASET" && zfs destroy -r "$DATASET" + log_must restore_tunable SNAPSHOT_LIST_BATCH_TIME_US + log_must restore_tunable SNAPSHOT_LIST_BATCH_SIZE +} + +function find_shim +{ + typeset helper helper_dir candidate + + helper=$(readlink -f "$(command -v snapshot_list_test)") + helper_dir=${helper%/*} + for candidate in \ + "$helper_dir/.libs/libsnapshot_list_test_shim.so" \ + "$helper_dir/libsnapshot_list_test_shim.so" \ + "$STF_SUITE/bin/libsnapshot_list_test_shim.so"; do + [[ -f "$candidate" ]] && print -- "$candidate" && return 0 + done + return 1 +} + +function compare_projected_json_int +{ + typeset dataset="$1" + typeset object_types="$2" + typeset preload="$SHIM" + typeset -i batch_calls + + [[ -n "$LD_PRELOAD" ]] && preload="$SHIM:$LD_PRELOAD" + log_must rm -f "$MARKER" + log_must eval "LD_PRELOAD='$preload' " \ + "ZFS_SNAPSHOT_LIST_TEST_MODE='count' " \ + "ZFS_SNAPSHOT_LIST_TEST_MARKER='$MARKER' " \ + "zfs list -j --json-int -t '$object_types' -d 1 -o '$COLUMNS' " \ + "'$dataset' > '$BATCH_OUTPUT'" + log_must grep -Fx count "$MARKER" + batch_calls=$(wc -l < "$MARKER") + (( batch_calls > 1 )) || log_fail \ + "one-microsecond time budget did not split snapshot iteration" + log_must eval "zfs list -j --json-int -t '$object_types' -d 1 " \ + "-s test:force-legacy-iterator -o '$COLUMNS' '$dataset' " \ + "> '$LEGACY_OUTPUT'" + log_must diff "$LEGACY_OUTPUT" "$BATCH_OUTPUT" + log_must rm -f "$MARKER" +} + +function compare_locked_encrypted +{ + typeset preload="$SHIM" + + [[ -n "$LD_PRELOAD" ]] && preload="$SHIM:$LD_PRELOAD" + log_must rm -f "$MARKER" + log_must eval "LD_PRELOAD='$preload' " \ + "ZFS_SNAPSHOT_LIST_TEST_MODE='count' " \ + "ZFS_SNAPSHOT_LIST_TEST_MARKER='$MARKER' " \ + "zfs list -H -p -t snapshot -o '$COLUMNS' " \ + "'$ENCRYPTED_DATASET' > '$BATCH_OUTPUT'" + log_must grep -Fx count "$MARKER" + log_must eval "zfs list -H -p -t snapshot " \ + "-s test:force-legacy-iterator -o '$COLUMNS' " \ + "'$ENCRYPTED_DATASET' > '$LEGACY_OUTPUT'" + log_must diff "$LEGACY_OUTPUT" "$BATCH_OUTPUT" + log_must rm -f "$MARKER" +} + +function compare_recursive_hybrid +{ + typeset object_types="filesystem,volume,snapshot,bookmark" + typeset columns="name,createtxg,guid,compression,quota,sharenfs" + typeset sort_options + typeset preload="$SHIM" + + [[ -n "$LD_PRELOAD" ]] && preload="$SHIM:$LD_PRELOAD" + for sort_options in "" "-s quota" "-s sharenfs"; do + log_must rm -f "$MARKER" + log_must eval "LD_PRELOAD='$preload' " \ + "ZFS_SNAPSHOT_LIST_TEST_MODE='count' " \ + "ZFS_SNAPSHOT_LIST_TEST_MARKER='$MARKER' " \ + "zfs list -H -p -r -t '$object_types' -o '$columns' " \ + "$sort_options '$MIXED_DATASET' > '$BATCH_OUTPUT'" + log_must grep -Fx count "$MARKER" + log_must eval "zfs list -H -p -r -t '$object_types' " \ + "-o '$columns' $sort_options " \ + "-s test:force-legacy-iterator '$MIXED_DATASET' " \ + "> '$LEGACY_OUTPUT'" + log_must diff "$LEGACY_OUTPUT" "$BATCH_OUTPUT" + done + log_must rm -f "$MARKER" +} + +function verify_nonrecursive_mixed_scope +{ + typeset columns="name,createtxg,guid" + typeset preload="$SHIM" + + [[ -n "$LD_PRELOAD" ]] && preload="$SHIM:$LD_PRELOAD" + log_must rm -f "$MARKER" + log_must eval "LD_PRELOAD='$preload' " \ + "ZFS_SNAPSHOT_LIST_TEST_MODE='count' " \ + "ZFS_SNAPSHOT_LIST_TEST_MARKER='$MARKER' " \ + "zfs list -H -p -t snapshot,bookmark -d 1 -o '$columns' " \ + "'$MIXED_DATASET' > '$BATCH_OUTPUT'" + log_must grep -Fx count "$MARKER" + printf "%s\n" "$MIXED_DATASET@root" "$MIXED_DATASET#root" | \ + sort > "$EXPECTED_OUTPUT" + log_must eval "cut -f1 '$BATCH_OUTPUT' | sort > '$LEGACY_OUTPUT'" + log_must diff "$EXPECTED_OUTPUT" "$LEGACY_OUTPUT" + + log_must eval "zfs list -H -p -t snapshot,bookmark -d 1 " \ + "-o '$columns' '$MIXED_DATASET' '$MIXED_CHILD' " \ + "> '$BATCH_OUTPUT'" + printf "%s\n" "$MIXED_DATASET@root" "$MIXED_DATASET#root" \ + "$MIXED_CHILD@child" "$MIXED_CHILD#child" | \ + sort > "$EXPECTED_OUTPUT" + log_must eval "cut -f1 '$BATCH_OUTPUT' | sort > '$LEGACY_OUTPUT'" + log_must diff "$EXPECTED_OUTPUT" "$LEGACY_OUTPUT" + + log_must eval "zfs list -H -p -t filesystem,snapshot,bookmark " \ + "-o name '$MIXED_DATASET' > '$BATCH_OUTPUT'" + printf "%s\n" "$MIXED_DATASET" > "$EXPECTED_OUTPUT" + log_must diff "$EXPECTED_OUTPUT" "$BATCH_OUTPUT" + log_must rm -f "$MARKER" +} + +function compare_deferred_snapshot +{ + typeset preload="$SHIM" + typeset userrefs + typeset -i matches + + [[ -n "$LD_PRELOAD" ]] && preload="$SHIM:$LD_PRELOAD" + log_must rm -f "$MARKER" + log_must eval "LD_PRELOAD='$preload' " \ + "ZFS_SNAPSHOT_LIST_TEST_MODE='count' " \ + "ZFS_SNAPSHOT_LIST_TEST_MARKER='$MARKER' " \ + "zfs list -H -p -t snapshot -o '$COLUMNS' " \ + "'$DATASET' > '$BATCH_OUTPUT'" + log_must grep -Fx count "$MARKER" + log_must eval "zfs list -H -p -t snapshot " \ + "-s test:force-legacy-iterator -o '$COLUMNS' '$DATASET' " \ + "> '$LEGACY_OUTPUT'" + log_must diff "$LEGACY_OUTPUT" "$BATCH_OUTPUT" + + matches=$(awk -F '\t' -v target="$DATASET@deferred" \ + '$4 == target { count++ } END { print count + 0 }' "$BATCH_OUTPUT") + (( matches == 1 )) || log_fail \ + "deferred-destroy snapshot appeared $matches times; expected once" + userrefs=$(awk -F '\t' -v target="$DATASET@deferred" \ + '$4 == target { print $7 }' "$BATCH_OUTPUT") + [[ "$userrefs" == "1" ]] || log_fail \ + "deferred-destroy snapshot has userrefs=$userrefs; expected 1" + log_must rm -f "$MARKER" +} + +function compare_implicit_snapshots +{ + typeset columns="name,createtxg,guid" + typeset preload="$SHIM" + + [[ -n "$LD_PRELOAD" ]] && preload="$SHIM:$LD_PRELOAD" + saved_listsnapshots=$(get_pool_prop listsnapshots "$TESTPOOL") + log_must zpool set listsnapshots=on "$TESTPOOL" + log_must rm -f "$MARKER" + log_must eval "LD_PRELOAD='$preload' " \ + "ZFS_SNAPSHOT_LIST_TEST_MODE='count' " \ + "ZFS_SNAPSHOT_LIST_TEST_MARKER='$MARKER' " \ + "zfs list -H -p -r -o '$columns' '$MIXED_DATASET' " \ + "> '$BATCH_OUTPUT'" + log_must grep -Fx count "$MARKER" + log_must eval "zfs list -H -p -r -o '$columns' " \ + "-s test:force-legacy-iterator '$MIXED_DATASET' " \ + "> '$LEGACY_OUTPUT'" + log_must diff "$LEGACY_OUTPUT" "$BATCH_OUTPUT" + + printf "%s\n" "$MIXED_DATASET" "$MIXED_DATASET@root" \ + "$MIXED_CHILD" "$MIXED_CHILD@child" "$MIXED_VOLUME" \ + "$MIXED_VOLUME@volume" | sort > "$EXPECTED_OUTPUT" + log_must eval "cut -f1 '$BATCH_OUTPUT' | sort > '$LEGACY_OUTPUT'" + log_must diff "$EXPECTED_OUTPUT" "$LEGACY_OUTPUT" + log_must rm -f "$MARKER" + log_must zpool set listsnapshots="$saved_listsnapshots" "$TESTPOOL" + saved_listsnapshots="" +} + +function compare_bookmark_only_mixed_type +{ + typeset columns="name,createtxg,guid" + typeset preload="$SHIM" + typeset actual + typeset -i lines + + [[ -n "$LD_PRELOAD" ]] && preload="$SHIM:$LD_PRELOAD" + log_must rm -f "$MARKER" + log_must eval "LD_PRELOAD='$preload' " \ + "ZFS_SNAPSHOT_LIST_TEST_MODE='count' " \ + "ZFS_SNAPSHOT_LIST_TEST_MARKER='$MARKER' " \ + "zfs list -H -p -t snapshot,bookmark -d 1 -o '$columns' " \ + "'$MIXED_CHILD' > '$BATCH_OUTPUT'" + log_must grep -Fx count "$MARKER" + log_must eval "zfs list -H -p -t snapshot,bookmark -d 1 " \ + "-s test:force-legacy-iterator -o '$columns' '$MIXED_CHILD' " \ + "> '$LEGACY_OUTPUT'" + log_must diff "$LEGACY_OUTPUT" "$BATCH_OUTPUT" + + lines=$(wc -l < "$BATCH_OUTPUT") + (( lines == 1 )) || log_fail \ + "bookmark-only mixed list returned $lines entries; expected one" + actual=$(cut -f1 "$BATCH_OUTPUT") + [[ "$actual" == "$MIXED_CHILD#child" ]] || log_fail \ + "bookmark-only mixed list returned $actual" + log_must rm -f "$MARKER" +} + +SHIM=$(find_shim) || log_unsupported "snapshot-list test shim not found" +log_onexit cleanup +log_assert "Projected listing matches legacy output across operational states." + +log_must save_tunable SNAPSHOT_LIST_BATCH_SIZE +log_must save_tunable SNAPSHOT_LIST_BATCH_TIME_US +log_mustnot set_tunable32 SNAPSHOT_LIST_BATCH_SIZE 0 +log_mustnot set_tunable32 SNAPSHOT_LIST_BATCH_TIME_US 0 +log_mustnot set_tunable32 SNAPSHOT_LIST_BATCH_SIZE 4097 +log_mustnot set_tunable32 SNAPSHOT_LIST_BATCH_TIME_US 100001 +log_must set_tunable32 SNAPSHOT_LIST_BATCH_SIZE 4096 +log_must set_tunable32 SNAPSHOT_LIST_BATCH_TIME_US 1 + +log_must zfs create "$DATASET" +log_must file_write -o create -f "$DATASET_MOUNT/payload" \ + -b 131072 -c 8 -d R +log_must zfs snapshot "$DATASET@base" +log_must file_write -o append -f "$DATASET_MOUNT/payload" \ + -b 131072 -c 4 -d R +log_must zfs snapshot "$DATASET@after_append" +log_must rm "$DATASET_MOUNT/payload" +log_must zfs snapshot "$DATASET@after_remove" +log_must zfs hold "$HOLD_TAG" "$DATASET@after_append" +log_must zfs bookmark "$DATASET@base" "$DATASET#base" +log_must zfs bookmark "$DATASET@after_append" "$DATASET#after_append" +log_must zfs snapshot "$DATASET@bookmark_source" +log_must zfs bookmark "$DATASET@bookmark_source" \ + "$DATASET#source_destroyed" +log_must zfs destroy "$DATASET@bookmark_source" +snapexists "$DATASET@bookmark_source" && \ + log_fail "bookmark source snapshot still exists" + +compare_projected_json_int "$DATASET" snapshot,bookmark +log_must set_tunable32 SNAPSHOT_LIST_BATCH_SIZE 1 +log_must set_tunable32 SNAPSHOT_LIST_BATCH_TIME_US 100000 + +log_must zfs snapshot "$DATASET@deferred" +log_must zfs hold "$DEFERRED_HOLD_TAG" "$DATASET@deferred" +log_must zfs destroy -d "$DATASET@deferred" +defer_destroy=$(get_prop defer_destroy "$DATASET@deferred") +[[ "$defer_destroy" == "on" ]] || log_fail \ + "deferred snapshot has defer_destroy=$defer_destroy; expected on" +compare_deferred_snapshot +log_must zfs release "$DEFERRED_HOLD_TAG" "$DATASET@deferred" +snapexists "$DATASET@deferred" && \ + log_fail "deferred snapshot remained after its final hold was released" + +log_must eval "echo 'projected-list-password' | zfs create " \ + "-o encryption=on -o keyformat=passphrase -o keylocation=prompt " \ + "-o mountpoint=none '$ENCRYPTED_DATASET'" +log_must zfs snapshot "$ENCRYPTED_DATASET@first" +log_must zfs snapshot "$ENCRYPTED_DATASET@second" +log_must zfs hold "$ENCRYPTED_HOLD_TAG" "$ENCRYPTED_DATASET@second" +log_must snapshot_list_test encrypted-snapshot-metadata "$ENCRYPTED_DATASET" +log_must zfs unload-key "$ENCRYPTED_DATASET" +keystatus=$(get_prop keystatus "$ENCRYPTED_DATASET") +[[ "$keystatus" == "unavailable" ]] || \ + log_fail "encrypted dataset key remains $keystatus after unload" +log_must snapshot_list_test encrypted-snapshot-metadata "$ENCRYPTED_DATASET" +compare_locked_encrypted + +log_must zfs create -o mountpoint=none "$MIXED_DATASET" +log_must zfs create -o mountpoint=none "$MIXED_CHILD" +log_must zfs create -V 64M "$MIXED_VOLUME" +log_must zfs snapshot "$MIXED_DATASET@root" +log_must zfs snapshot "$MIXED_CHILD@child" +log_must zfs snapshot "$MIXED_VOLUME@volume" +log_must zfs bookmark "$MIXED_DATASET@root" "$MIXED_DATASET#root" +log_must zfs bookmark "$MIXED_CHILD@child" "$MIXED_CHILD#child" +log_must zfs bookmark "$MIXED_VOLUME@volume" "$MIXED_VOLUME#volume" +verify_nonrecursive_mixed_scope +compare_recursive_hybrid +compare_implicit_snapshots +log_must zfs destroy "$MIXED_CHILD@child" +snapexists "$MIXED_CHILD@child" && \ + log_fail "bookmark source snapshot still exists" +compare_bookmark_only_mixed_type + +log_pass "Projected listing matches legacy output across operational states." diff --git a/tests/zfs-tests/tests/functional/cli_root/zfs_list/zfs_list_011_pos.ksh b/tests/zfs-tests/tests/functional/cli_root/zfs_list/zfs_list_011_pos.ksh new file mode 100755 index 000000000000..b30663bd22af --- /dev/null +++ b/tests/zfs-tests/tests/functional/cli_root/zfs_list/zfs_list_011_pos.ksh @@ -0,0 +1,87 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# + +. $STF_SUITE/include/libtest.shlib + +# +# DESCRIPTION: +# Batched snapshot listing handles iteration boundaries and ordering across +# multiple result batches. +# +# STRATEGY: +# 1. Create 1025 snapshots and verify direct iteration returns each exactly +# once. +# 2. Verify direct-libzfs ordering across multiple small batches. +# + +verify_runnable "global" +set -o pipefail + +BOUNDARY_DATASET="$TESTPOOL/$TESTFS/projected_boundary" +SUBSET_DATASET="$TESTPOOL/$TESTFS/projected_subsets" +BATCH_OUTPUT="$TEST_BASE_DIR/projected_boundary_batch.$$" +FILTER_OUTPUT="$TEST_BASE_DIR/projected_boundary_filter.$$" +EXPECTED_OUTPUT="$TEST_BASE_DIR/projected_boundary_expected.$$" + +function cleanup +{ + rm -f "$BATCH_OUTPUT" "$FILTER_OUTPUT" "$EXPECTED_OUTPUT" + datasetexists "$SUBSET_DATASET" && zfs destroy -r "$SUBSET_DATASET" + datasetexists "$BOUNDARY_DATASET" && zfs destroy -r "$BOUNDARY_DATASET" + log_must restore_tunable SNAPSHOT_LIST_BATCH_TIME_US + log_must restore_tunable SNAPSHOT_LIST_BATCH_SIZE +} + +log_onexit cleanup +log_assert "Batched snapshot listing handles boundaries and ordering." + +log_must save_tunable SNAPSHOT_LIST_BATCH_SIZE +log_must save_tunable SNAPSHOT_LIST_BATCH_TIME_US +log_must set_tunable32 SNAPSHOT_LIST_BATCH_SIZE 1024 +log_must set_tunable32 SNAPSHOT_LIST_BATCH_TIME_US 100000 + +log_must zfs create "$BOUNDARY_DATASET" + +typeset -i index=0 +while (( index < 1025 )); do + suffix=$(printf "%04d" "$index") + log_must zfs snapshot "$BOUNDARY_DATASET@suffix_$suffix" + (( index += 1 )) +done + +log_must eval "snapshot_list_test filter '$BOUNDARY_DATASET' 0 0 " \ + "> '$FILTER_OUTPUT'" +typeset -i callbacks +callbacks=$(wc -l < "$FILTER_OUTPUT") +(( callbacks == 1025 )) || + log_fail "direct iteration delivered $callbacks callbacks; expected 1025" +# Sorting preserves duplicate names, so this compares the callback multisets. +log_must eval "sort '$FILTER_OUTPUT' > '$BATCH_OUTPUT'" +log_must eval "zfs list -H -p -t snapshot " \ + "-s test:force-legacy-iterator -o name '$BOUNDARY_DATASET' | " \ + "sort > '$EXPECTED_OUTPUT'" +log_must diff "$EXPECTED_OUTPUT" "$BATCH_OUTPUT" + +log_must set_tunable32 SNAPSHOT_LIST_BATCH_SIZE 2 +log_must zfs create "$SUBSET_DATASET" +log_must zfs snapshot "$SUBSET_DATASET@m_oldest" +log_must zfs snapshot "$SUBSET_DATASET@z_middle" +log_must zfs snapshot "$SUBSET_DATASET@a_newest" +oldest_txg=$(zfs get -H -p -o value createtxg \ + "$SUBSET_DATASET@m_oldest") +middle_txg=$(zfs get -H -p -o value createtxg \ + "$SUBSET_DATASET@z_middle") +newest_txg=$(zfs get -H -p -o value createtxg \ + "$SUBSET_DATASET@a_newest") +(( oldest_txg < middle_txg && middle_txg < newest_txg )) || + log_fail "ordering snapshots do not have increasing creation TXGs" + +printf "%s\n" "$SUBSET_DATASET@m_oldest" "$SUBSET_DATASET@z_middle" \ + "$SUBSET_DATASET@a_newest" > "$EXPECTED_OUTPUT" + +log_must eval "snapshot_list_test sorted '$SUBSET_DATASET' " \ + "> '$BATCH_OUTPUT'" +log_must diff "$EXPECTED_OUTPUT" "$BATCH_OUTPUT" + +log_pass "Batched snapshot listing handles boundaries and ordering." diff --git a/tests/zfs-tests/tests/functional/cli_root/zfs_list/zfs_list_012_pos.ksh b/tests/zfs-tests/tests/functional/cli_root/zfs_list/zfs_list_012_pos.ksh new file mode 100755 index 000000000000..1f5d347503a1 --- /dev/null +++ b/tests/zfs-tests/tests/functional/cli_root/zfs_list/zfs_list_012_pos.ksh @@ -0,0 +1,710 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# + +. $STF_SUITE/include/libtest.shlib + +# +# DESCRIPTION: +# Projected snapshot and bookmark listing preserves ordering while handling +# buffer growth, old kernels, interruption, and injected iterator errors. +# +# STRATEGY: +# 1. Exercise synthetic and kernel-reported ENOMEM destination-buffer growth. +# 2. Reject every batch ioctl with CMD_UNAVAIL, ENOTTY, or ENOTSUP and verify +# automatic legacy fallback. +# 3. Verify creation-TXG filters survive automatic legacy fallback. +# 4. Reject a projected property and verify automatic legacy fallback. +# 5. Verify written uses projected listing and -o all bypasses it. +# 6. Exercise projected numeric values used only as sort keys. +# 7. Exercise the same error paths with all projected columns and name alone. +# 8. Verify callback values which collide with special ioctl errors are +# returned unchanged by snapshot and bookmark iteration. +# 9. Treat ioctl ENOENT and ESRCH before or after results as normal end. +# 10. Inject EINTR and require libzfs to preserve it. +# 11. Require direct projected properties and fail closed on materialization +# ENOMEM. +# 12. Reject an ioctl after one callback without fallback or callback replay. +# 13. Continue after a valid empty, non-EOF batch with an advancing cursor. +# 14. Verify filtered-out snapshots do not consume result batch slots. +# 15. Verify old-kernel fallback preserves mixed snapshots and bookmarks, +# including a bookmark whose source snapshot has been destroyed. +# 16. Preserve bookmark iterator errors. +# 17. Reuse stale parent names with different dataset types and encryption +# states, and require projected handles to describe the replacements. +# 18. Reject missing or invalid projected parent and written metadata with +# EPROTO. +# 19. Project cheap snapshot properties in batches, including aliases, +# object IDs, written bytes, nonzero clone counts, and deferred destroy, +# and preserve fallback. +# 20. Read redacted state from a real redacted receive snapshot. +# 21. Use projected listing for default snapshot and bookmark columns. +# 22. Use full snapshot properties when coloring the available column. +# 23. Batch built-ins which apply to neither snapshots nor bookmarks, including +# as sort keys, while keeping pseudo-properties on legacy listing. +# + +verify_runnable "global" +set -o pipefail + +DATASET="$TESTPOOL/$TESTFS/projected_stress" +CLONE_DATASET="$TESTPOOL/$TESTFS/projected_stress_clone" +STALE_DESTROY_DATASET="$DATASET/stale_destroy" +STALE_RENAME_DATASET="$DATASET/stale_rename" +STALE_RENAMED_DATASET="$DATASET/stale_renamed" +STALE_TYPE_DATASET="$DATASET/stale_type" +STALE_ENCRYPTED_DATASET="$DATASET/stale_encrypted" +REDACT_SOURCE="$TESTPOOL/$TESTFS/projected_redact_source" +REDACT_CLONE="$TESTPOOL/$TESTFS/projected_redact_clone" +REDACT_RECV="$TESTPOOL/$TESTFS/projected_redact_recv" +BATCH_OUTPUT="$TEST_BASE_DIR/projected_stress_batch.$$" +INJECTED_OUTPUT="$TEST_BASE_DIR/projected_stress_injected.$$" +EXPECTED_OUTPUT="$TEST_BASE_DIR/projected_stress_expected.$$" +MARKER="$TEST_BASE_DIR/projected_stress_marker.$$" +KEY_FILE="$TEST_BASE_DIR/projected_stress_key.$$" +COLUMNS="createtxg,creation,guid,name,written,type,userrefs,objsetid" +INAPPLICABLE_PROPERTIES="checksum compression dedup recordsize quota" +INAPPLICABLE_PROPERTIES="$INAPPLICABLE_PROPERTIES reservation readonly" +INAPPLICABLE_PROPERTIES="$INAPPLICABLE_PROPERTIES canmount sharenfs" +HOLD_TAG_ONE="projected-stress-one" +HOLD_TAG_TWO="projected-stress-two" + +function cleanup +{ + if snapexists "$DATASET@z_middle"; then + zfs release "$HOLD_TAG_ONE" "$DATASET@z_middle" \ + >/dev/null 2>&1 + zfs release "$HOLD_TAG_TWO" "$DATASET@z_middle" \ + >/dev/null 2>&1 + fi + rm -f "$BATCH_OUTPUT" "$INJECTED_OUTPUT" "$EXPECTED_OUTPUT" \ + "$MARKER" "$KEY_FILE" + datasetexists "$REDACT_RECV" && zfs destroy -r "$REDACT_RECV" + datasetexists "$REDACT_CLONE" && zfs destroy -r "$REDACT_CLONE" + datasetexists "$REDACT_SOURCE" && zfs destroy -r "$REDACT_SOURCE" + datasetexists "$CLONE_DATASET" && zfs destroy -r "$CLONE_DATASET" + datasetexists "$DATASET" && zfs destroy -r "$DATASET" + log_must restore_tunable SNAPSHOT_LIST_BATCH_TIME_US + log_must restore_tunable SNAPSHOT_LIST_BATCH_SIZE +} + +function find_shim +{ + typeset helper helper_dir candidate + + helper=$(readlink -f "$(command -v snapshot_list_test)") + helper_dir=${helper%/*} + for candidate in \ + "$helper_dir/.libs/libsnapshot_list_test_shim.so" \ + "$helper_dir/libsnapshot_list_test_shim.so" \ + "$STF_SUITE/bin/libsnapshot_list_test_shim.so"; do + [[ -f "$candidate" ]] && print -- "$candidate" && return 0 + done + return 1 +} + +function run_injected_list +{ + typeset mode="$1" + typeset output="$2" + typeset columns="${3:-$COLUMNS}" + typeset expected="${4:-$BATCH_OUTPUT}" + typeset sort_options="$5" + typeset object_types="${6:-snapshot}" + typeset preload="$SHIM" + + [[ -n "$LD_PRELOAD" ]] && preload="$SHIM:$LD_PRELOAD" + log_must eval "LD_PRELOAD='$preload' " \ + "ZFS_SNAPSHOT_LIST_TEST_MODE='$mode' " \ + "ZFS_SNAPSHOT_LIST_TEST_MARKER='$MARKER' " \ + "zfs list -H -p -t '$object_types' -o '$columns' " \ + "$sort_options " \ + "'$DATASET' " \ + "> '$output'" + log_must grep -Fx "$mode" "$MARKER" + log_must diff "$expected" "$output" + log_must rm -f "$MARKER" +} + +function verify_projected_written +{ + typeset preload="$SHIM" + + [[ -n "$LD_PRELOAD" ]] && preload="$SHIM:$LD_PRELOAD" + log_must rm -f "$MARKER" + log_must eval "zfs list -H -p -t snapshot " \ + "-s test:force-legacy-iterator -o name,written '$DATASET' " \ + "> '$EXPECTED_OUTPUT'" + log_must eval "LD_PRELOAD='$preload' " \ + "ZFS_SNAPSHOT_LIST_TEST_MODE='count' " \ + "ZFS_SNAPSHOT_LIST_TEST_MARKER='$MARKER' " \ + "zfs list -H -p -t snapshot -o name,written '$DATASET' " \ + "> '$INJECTED_OUTPUT'" + log_must grep -Fx count "$MARKER" + log_must diff "$EXPECTED_OUTPUT" "$INJECTED_OUTPUT" + log_must rm -f "$MARKER" + + for sort_options in "-s written" "-S written"; do + log_must eval "zfs list -H -p -t snapshot -o name " \ + "$sort_options -s test:force-legacy-iterator " \ + "'$DATASET' > '$EXPECTED_OUTPUT'" + log_must eval "LD_PRELOAD='$preload' " \ + "ZFS_SNAPSHOT_LIST_TEST_MODE='count' " \ + "ZFS_SNAPSHOT_LIST_TEST_MARKER='$MARKER' " \ + "zfs list -H -p -t snapshot -o name $sort_options " \ + "'$DATASET' > '$INJECTED_OUTPUT'" + log_must grep -Fx count "$MARKER" + log_must diff "$EXPECTED_OUTPUT" "$INJECTED_OUTPUT" + log_must rm -f "$MARKER" + done +} + +function verify_unprojected_properties_use_legacy +{ + typeset property + typeset preload="$SHIM" + + [[ -n "$LD_PRELOAD" ]] && preload="$SHIM:$LD_PRELOAD" + log_must rm -f "$MARKER" + log_must eval "zfs list -H -p -t snapshot,bookmark -d 1 -o all " \ + "'$DATASET' > '$EXPECTED_OUTPUT'" + log_must eval "LD_PRELOAD='$preload' " \ + "ZFS_SNAPSHOT_LIST_TEST_MODE='count' " \ + "ZFS_SNAPSHOT_LIST_TEST_MARKER='$MARKER' " \ + "zfs list -H -p -t snapshot,bookmark -d 1 -o all '$DATASET' " \ + "> '$INJECTED_OUTPUT'" + log_must diff "$EXPECTED_OUTPUT" "$INJECTED_OUTPUT" + [[ ! -e "$MARKER" ]] || + log_fail "-o all used projected listing" + + for property in test:force-legacy-iterator userquota@0 \ + written@m_oldest; do + log_must rm -f "$MARKER" + log_must eval "LD_PRELOAD='$preload' " \ + "ZFS_SNAPSHOT_LIST_TEST_MODE='count' " \ + "ZFS_SNAPSHOT_LIST_TEST_MARKER='$MARKER' " \ + "zfs list -H -p -t snapshot -o 'name,$property' " \ + "'$DATASET' > /dev/null" + [[ ! -e "$MARKER" ]] || log_fail \ + "$property used projected listing" + done + + log_must rm -f "$MARKER" + log_must eval "LD_PRELOAD='$preload' " \ + "ZFS_SNAPSHOT_LIST_TEST_MODE='count' " \ + "ZFS_SNAPSHOT_LIST_TEST_MARKER='$MARKER' " \ + "zfs list -H -p -t snapshot -s test:force-legacy-iterator " \ + "-o name '$DATASET' > /dev/null" + [[ ! -e "$MARKER" ]] || log_fail \ + "user-property sort used projected listing" +} + +function run_injected_interrupt +{ + typeset preload="$SHIM" + + [[ -n "$LD_PRELOAD" ]] && preload="$SHIM:$LD_PRELOAD" + log_must eval "LD_PRELOAD='$preload' " \ + "ZFS_SNAPSHOT_LIST_TEST_MODE='eintr' " \ + "ZFS_SNAPSHOT_LIST_TEST_MARKER='$MARKER' " \ + "snapshot_list_test interrupt '$DATASET'" + log_must grep -Fx eintr "$MARKER" + log_must rm -f "$MARKER" +} + +function verify_direct_properties +{ + typeset preload="$SHIM" + + [[ -n "$LD_PRELOAD" ]] && preload="$SHIM:$LD_PRELOAD" + log_must eval "LD_PRELOAD='$preload' " \ + "ZFS_SNAPSHOT_LIST_TEST_MODE='direct_properties' " \ + "ZFS_SNAPSHOT_LIST_TEST_MARKER='$MARKER' " \ + "snapshot_list_test direct-properties '$DATASET'" + log_must grep -Fx direct_properties "$MARKER" + ! grep -q -Fx unexpected_property_nvlist "$MARKER" || + log_fail "projected handle built per-property nvlists" + log_must rm -f "$MARKER" + log_must snapshot_list_test materialized-properties "$DATASET" + log_must eval "LD_PRELOAD='$preload' " \ + "ZFS_SNAPSHOT_LIST_TEST_MODE='direct_properties' " \ + "ZFS_SNAPSHOT_LIST_TEST_MARKER='$MARKER' " \ + "snapshot_list_test refreshed-properties '$DATASET'" + ! grep -q -Fx unexpected_property_nvlist "$MARKER" || + log_fail "refreshed handle retained projected properties" + log_must rm -f "$MARKER" +} + +function run_injected_handle_enomem +{ + typeset preload="$SHIM" + + [[ -n "$LD_PRELOAD" ]] && preload="$SHIM:$LD_PRELOAD" + log_must eval "LD_PRELOAD='$preload' " \ + "ZFS_SNAPSHOT_LIST_TEST_MODE='handle_enomem' " \ + "ZFS_SNAPSHOT_LIST_TEST_MARKER='$MARKER' " \ + "snapshot_list_test handle-enomem '$DATASET'" + log_must grep -Fx handle_enomem "$MARKER" + log_must rm -f "$MARKER" +} + +function run_injected_metadata_errors +{ + typeset mode preload="$SHIM" + + [[ -n "$LD_PRELOAD" ]] && preload="$SHIM:$LD_PRELOAD" + for mode in missing_dmu_type invalid_dmu_type missing_dds_flags \ + invalid_dds_flags missing_writtens missing_written_valid \ + short_writtens short_written_valid invalid_written_valid; do + log_must rm -f "$MARKER" + log_must eval "LD_PRELOAD='$preload' " \ + "ZFS_SNAPSHOT_LIST_TEST_MODE='$mode' " \ + "ZFS_SNAPSHOT_LIST_TEST_MARKER='$MARKER' " \ + "snapshot_list_test metadata-eproto '$DATASET'" + log_must grep -Fx "$mode" "$MARKER" + done + log_must rm -f "$MARKER" +} + +function run_injected_late_failure +{ + typeset preload="$SHIM" + typeset -i lines + + [[ -n "$LD_PRELOAD" ]] && preload="$SHIM:$LD_PRELOAD" + log_mustnot eval "LD_PRELOAD='$preload' " \ + "ZFS_SNAPSHOT_LIST_TEST_MODE='enotsup_after_first' " \ + "ZFS_SNAPSHOT_LIST_TEST_MARKER='$MARKER' " \ + "snapshot_list_test filter '$DATASET' 0 0 > '$INJECTED_OUTPUT'" + log_must grep -Fx enotsup_after_first "$MARKER" + lines=$(wc -l < "$INJECTED_OUTPUT") + (( lines == 1 )) || + log_fail "late batch failure delivered $lines snapshots; expected 1" + log_must rm -f "$MARKER" +} + +function run_injected_empty_batch +{ + typeset preload="$SHIM" + typeset -i lines + + [[ -n "$LD_PRELOAD" ]] && preload="$SHIM:$LD_PRELOAD" + log_must eval "LD_PRELOAD='$preload' " \ + "ZFS_SNAPSHOT_LIST_TEST_MODE='empty_non_eof' " \ + "ZFS_SNAPSHOT_LIST_TEST_MARKER='$MARKER' " \ + "snapshot_list_test filter '$DATASET' 0 0 " \ + "> '$INJECTED_OUTPUT'" + log_must grep -Fx empty_non_eof "$MARKER" + # The shim consumes one result to obtain the kernel's opaque next cursor. + lines=$(wc -l < "$INJECTED_OUTPUT") + (( lines == 2 )) || + log_fail "iteration after an empty batch delivered $lines snapshots; " \ + "expected 2" + log_must rm -f "$MARKER" +} + +function run_injected_eof_after_first +{ + typeset mode + typeset preload="$SHIM" + + [[ -n "$LD_PRELOAD" ]] && preload="$SHIM:$LD_PRELOAD" + log_must eval "snapshot_list_test filter '$DATASET' 0 0 " \ + "> '$BATCH_OUTPUT'" + log_must eval "head -n 1 '$BATCH_OUTPUT' > '$EXPECTED_OUTPUT'" + [[ -s "$EXPECTED_OUTPUT" ]] || + log_fail "unmodified iterator returned no first snapshot" + + for mode in enoent_after_first esrch_after_first; do + log_must rm -f "$MARKER" + log_must eval "LD_PRELOAD='$preload' " \ + "ZFS_SNAPSHOT_LIST_TEST_MODE='$mode' " \ + "ZFS_SNAPSHOT_LIST_TEST_MARKER='$MARKER' " \ + "snapshot_list_test filter '$DATASET' 0 0 " \ + "> '$INJECTED_OUTPUT'" + log_must grep -Fx "$mode" "$MARKER" + log_must diff "$EXPECTED_OUTPUT" "$INJECTED_OUTPUT" + log_must rm -f "$MARKER" + done +} + +function run_injected_bookmark_errors +{ + typeset error mode preload="$SHIM" + typeset -i calls + + [[ -n "$LD_PRELOAD" ]] && preload="$SHIM:$LD_PRELOAD" + for error in eio enoent esrch; do + mode="bookmark_$error" + log_must rm -f "$MARKER" + log_must eval "LD_PRELOAD='$preload' " \ + "ZFS_SNAPSHOT_LIST_TEST_MODE='$mode' " \ + "ZFS_SNAPSHOT_LIST_TEST_MARKER='$MARKER' " \ + "snapshot_list_test bookmark-error '$DATASET' '$error'" + calls=$(grep -Fxc "$mode" "$MARKER") + (( calls == 3 )) || + log_fail "$mode was injected $calls times; expected 3" + done + log_must rm -f "$MARKER" +} + +function verify_stale_bookmark_handles +{ + log_must zfs create -o mountpoint=none "$STALE_DESTROY_DATASET" + log_must snapshot_list_test stale-bookmarks-destroy \ + "$STALE_DESTROY_DATASET" + datasetexists "$STALE_DESTROY_DATASET" && + log_fail "stale-handle destroy left $STALE_DESTROY_DATASET" + + log_must zfs create -o mountpoint=none "$STALE_RENAME_DATASET" + log_must snapshot_list_test stale-bookmarks-rename \ + "$STALE_RENAME_DATASET" "$STALE_RENAMED_DATASET" + datasetexists "$STALE_RENAME_DATASET" && + log_fail "stale-handle rename left $STALE_RENAME_DATASET" + datasetexists "$STALE_RENAMED_DATASET" || + log_fail "stale-handle rename did not create $STALE_RENAMED_DATASET" + log_must zfs destroy "$STALE_RENAMED_DATASET" +} + +function verify_stale_snapshot_metadata +{ + log_must zfs create -o mountpoint=none "$STALE_TYPE_DATASET" + log_must snapshot_list_test stale-snapshot-metadata \ + "$STALE_TYPE_DATASET" volume + destroy_dataset "$STALE_TYPE_DATASET" -r + + print 'projected-listing-passphrase' > "$KEY_FILE" + log_must chmod 600 "$KEY_FILE" + log_must zfs create -o mountpoint=none -o encryption=on \ + -o keyformat=passphrase -o keylocation="file://$KEY_FILE" \ + "$STALE_ENCRYPTED_DATASET" + log_must snapshot_list_test stale-snapshot-metadata \ + "$STALE_ENCRYPTED_DATASET" filesystem + destroy_dataset "$STALE_ENCRYPTED_DATASET" -r + log_must rm -f "$KEY_FILE" +} + +function verify_filtered_fallback +{ + typeset target="$DATASET@a_newest" + typeset txg preload="$SHIM" + + [[ -n "$LD_PRELOAD" ]] && preload="$SHIM:$LD_PRELOAD" + txg=$(zfs get -H -p -o value createtxg "$target") + log_must eval "LD_PRELOAD='$preload' " \ + "ZFS_SNAPSHOT_LIST_TEST_MODE='cmd_unavail' " \ + "ZFS_SNAPSHOT_LIST_TEST_MARKER='$MARKER' " \ + "snapshot_list_test filter '$DATASET' '$txg' '$txg' " \ + "> '$INJECTED_OUTPUT'" + log_must grep -Fx cmd_unavail "$MARKER" + [[ "$(<"$INJECTED_OUTPUT")" == "$target" ]] || + log_fail "legacy fallback did not preserve exact creation-TXG filter" + log_must rm -f "$MARKER" +} + +function verify_projected_sort +{ + typeset sort_options="$1" + + log_must eval "zfs list -H -p -t snapshot -o name " \ + "$sort_options -s test:force-legacy-iterator '$DATASET' " \ + "> '$EXPECTED_OUTPUT'" + run_injected_list count "$INJECTED_OUTPUT" name "$EXPECTED_OUTPUT" \ + "$sort_options" +} + +function verify_projected_userrefs +{ + log_must eval "zfs list -H -p -t snapshot " \ + "-s test:force-legacy-iterator -o name,userrefs '$DATASET' " \ + "> '$EXPECTED_OUTPUT'" + run_injected_list count "$INJECTED_OUTPUT" name,userrefs \ + "$EXPECTED_OUTPUT" +} + +function verify_default_property_lists +{ + typeset object_types + typeset preload="$SHIM" + + [[ -n "$LD_PRELOAD" ]] && preload="$SHIM:$LD_PRELOAD" + for object_types in snapshot snapshot,bookmark; do + log_must eval "zfs list -H -p -t '$object_types' -d 1 " \ + "-s test:force-legacy-iterator " \ + "-o name,used,available,referenced,mountpoint " \ + "'$DATASET' > '$EXPECTED_OUTPUT'" + log_must eval "LD_PRELOAD='$preload' " \ + "ZFS_SNAPSHOT_LIST_TEST_MODE='count' " \ + "ZFS_SNAPSHOT_LIST_TEST_MARKER='$MARKER' " \ + "zfs list -H -p -t '$object_types' -d 1 '$DATASET' " \ + "> '$INJECTED_OUTPUT'" + log_must grep -Fx count "$MARKER" + log_must diff "$EXPECTED_OUTPUT" "$INJECTED_OUTPUT" + log_must rm -f "$MARKER" + done + + log_must eval "zfs list -H -p -t bookmark " \ + "-s test:force-legacy-iterator " \ + "-o name,used,available,referenced,mountpoint '$DATASET' " \ + "> '$EXPECTED_OUTPUT'" + log_must eval "LD_PRELOAD='$preload' " \ + "ZFS_SNAPSHOT_LIST_TEST_MODE='bookmark_projected' " \ + "ZFS_SNAPSHOT_LIST_TEST_MARKER='$MARKER' " \ + "zfs list -H -p -t bookmark '$DATASET' > '$INJECTED_OUTPUT'" + log_must grep -Fx bookmark_projected "$MARKER" + ! grep -q -Fx bookmark_not_projected "$MARKER" || + log_fail "default bookmark listing requested excess properties" + log_must diff "$EXPECTED_OUTPUT" "$INJECTED_OUTPUT" + log_must rm -f "$MARKER" +} + +function verify_colored_snapshot_listing_uses_legacy +{ + typeset object_types + typeset preload="$SHIM" + + [[ -n "$LD_PRELOAD" ]] && preload="$SHIM:$LD_PRELOAD" + for object_types in snapshot snapshot,bookmark; do + log_must rm -f "$MARKER" + log_must eval "faketty TERM=xterm-256color ZFS_COLOR=1 " \ + "zfs list -t '$object_types' -d 1 " \ + "-s test:force-legacy-iterator '$DATASET' " \ + "> '$EXPECTED_OUTPUT'" + log_must eval "faketty TERM=xterm-256color ZFS_COLOR=1 " \ + "LD_PRELOAD='$preload' " \ + "ZFS_SNAPSHOT_LIST_TEST_MODE='count' " \ + "ZFS_SNAPSHOT_LIST_TEST_MARKER='$MARKER' " \ + "zfs list -t '$object_types' -d 1 '$DATASET' " \ + "> '$INJECTED_OUTPUT'" + [[ ! -e "$MARKER" ]] || log_fail \ + "colored $object_types listing used projected properties" + log_must diff "$EXPECTED_OUTPUT" "$INJECTED_OUTPUT" + done +} + +function verify_projected_property_union +{ + typeset columns="name,used,available,referenced,refer,mountpoint" + typeset property + typeset numclones + columns="$columns,logicalused,lused,logicalreferenced,lrefer" + columns="$columns,defer_destroy,numclones,objsetid" + columns="$columns,inconsistent,redacted,origin,written" + for property in $INAPPLICABLE_PROPERTIES; do + columns="$columns,$property" + done + + log_must eval "zfs list -H -p -t snapshot " \ + "-s test:force-legacy-iterator -o '$columns' '$DATASET' " \ + "> '$EXPECTED_OUTPUT'" + run_injected_list count "$INJECTED_OUTPUT" "$columns" \ + "$EXPECTED_OUTPUT" + run_injected_list arg_unavail "$INJECTED_OUTPUT" "$columns" \ + "$EXPECTED_OUTPUT" + + for property in used available referenced refer mountpoint logicalused \ + lused logicalreferenced lrefer defer_destroy numclones inconsistent \ + redacted origin written objsetid $INAPPLICABLE_PROPERTIES; do + log_must eval "zfs list -H -p -t snapshot " \ + "-s '$property' -s test:force-legacy-iterator " \ + "-o name '$DATASET' > '$EXPECTED_OUTPUT'" + run_injected_list count "$INJECTED_OUTPUT" name \ + "$EXPECTED_OUTPUT" "-s $property" + done + + numclones=$(zfs get -H -p -o value numclones \ + "$DATASET@m_oldest") + (( numclones == 1 )) || + log_fail "projected numclones is $numclones; expected 1" +} + +function verify_inapplicable_property_types +{ + typeset columns="name,logicalused,lused" + typeset object_types property + for property in $INAPPLICABLE_PROPERTIES; do + columns="$columns,$property" + done + + for object_types in snapshot snapshot,bookmark; do + log_must eval "zfs list -H -p -t '$object_types' -d 1 " \ + "-s test:force-legacy-iterator -o '$columns' '$DATASET' " \ + "> '$EXPECTED_OUTPUT'" + run_injected_list count "$INJECTED_OUTPUT" "$columns" \ + "$EXPECTED_OUTPUT" "-d 1" "$object_types" + done + + log_must eval "zfs list -H -p -t bookmark " \ + "-s test:force-legacy-iterator -o '$columns' '$DATASET' " \ + "> '$EXPECTED_OUTPUT'" + run_injected_list bookmark_empty_projected "$INJECTED_OUTPUT" \ + "$columns" "$EXPECTED_OUTPUT" "" bookmark +} + +function verify_projected_redacted +{ + typeset columns="name,numclones,inconsistent,redacted,origin" + typeset preload="$SHIM" + typeset redacted + + [[ -n "$LD_PRELOAD" ]] && preload="$SHIM:$LD_PRELOAD" + log_must zfs create -o mountpoint=none "$REDACT_SOURCE" + log_must zfs snapshot "$REDACT_SOURCE@base" + log_must zfs clone -o mountpoint=none "$REDACT_SOURCE@base" \ + "$REDACT_CLONE" + log_must zfs snapshot "$REDACT_CLONE@mask" + log_must zfs redact "$REDACT_SOURCE@base" book \ + "$REDACT_CLONE@mask" + log_must eval "zfs send --redact book '$REDACT_SOURCE@base' | " \ + "zfs receive -u '$REDACT_RECV'" + + log_must eval "zfs list -H -p -t snapshot " \ + "-s test:force-legacy-iterator -o '$columns' '$REDACT_RECV' " \ + "> '$EXPECTED_OUTPUT'" + log_must eval "LD_PRELOAD='$preload' " \ + "ZFS_SNAPSHOT_LIST_TEST_MODE='count' " \ + "ZFS_SNAPSHOT_LIST_TEST_MARKER='$MARKER' " \ + "zfs list -H -p -t snapshot -o '$columns' " \ + "'$REDACT_RECV' > '$INJECTED_OUTPUT'" + log_must grep -Fx count "$MARKER" + log_must diff "$EXPECTED_OUTPUT" "$INJECTED_OUTPUT" + redacted=$(awk -F '\t' '{ print $4 }' "$INJECTED_OUTPUT") + [[ "$redacted" == "1" ]] || + log_fail "projected redacted is $redacted; expected 1" + log_must rm -f "$MARKER" +} + +function verify_filtered_batch_fill +{ + typeset target="$DATASET@a_newest" + typeset txg preload="$SHIM" + typeset -i calls + + [[ -n "$LD_PRELOAD" ]] && preload="$SHIM:$LD_PRELOAD" + txg=$(zfs get -H -p -o value createtxg "$target") + log_must eval "LD_PRELOAD='$preload' " \ + "ZFS_SNAPSHOT_LIST_TEST_MODE='count' " \ + "ZFS_SNAPSHOT_LIST_TEST_MARKER='$MARKER' " \ + "snapshot_list_test filter '$DATASET' '$txg' '$txg' " \ + "> '$INJECTED_OUTPUT'" + [[ "$(<"$INJECTED_OUTPUT")" == "$target" ]] || + log_fail "filtered batch did not return only $target" + calls=$(wc -l < "$MARKER") + (( calls == 1 )) || + log_fail "filtered result required $calls batches; expected 1" + log_must rm -f "$MARKER" +} + +function verify_mixed_type_fallback +{ + typeset columns="name,createtxg,guid" + + log_must eval "zfs list -H -p -t snapshot,bookmark -d 1 " \ + "-s test:force-legacy-iterator -o '$columns' '$DATASET' " \ + "> '$EXPECTED_OUTPUT'" + run_injected_list cmd_unavail "$INJECTED_OUTPUT" "$columns" \ + "$EXPECTED_OUTPUT" "-d 1" snapshot,bookmark + + printf "%s\n" "$DATASET@m_oldest" "$DATASET@z_middle" \ + "$DATASET@a_newest" "$DATASET#normal" \ + "$DATASET#source_destroyed" | sort > "$EXPECTED_OUTPUT" + log_must eval "cut -f1 '$INJECTED_OUTPUT' | sort > '$BATCH_OUTPUT'" + log_must diff "$EXPECTED_OUTPUT" "$BATCH_OUTPUT" + log_must rm -f "$EXPECTED_OUTPUT" +} + +SHIM=$(find_shim) || log_unsupported "snapshot-list test shim not found" +log_onexit cleanup +log_assert "Projected listing preserves ordering through error paths." + +log_must save_tunable SNAPSHOT_LIST_BATCH_SIZE +log_must save_tunable SNAPSHOT_LIST_BATCH_TIME_US +log_must set_tunable32 SNAPSHOT_LIST_BATCH_SIZE 1024 +log_must set_tunable32 SNAPSHOT_LIST_BATCH_TIME_US 100000 + +log_must zfs create "$DATASET" +log_must zfs snapshot "$DATASET@m_oldest" +log_must zfs snapshot "$DATASET@z_middle" +log_must zfs snapshot "$DATASET@a_newest" +log_must zfs hold "$HOLD_TAG_ONE" "$DATASET@z_middle" +log_must zfs hold "$HOLD_TAG_TWO" "$DATASET@z_middle" +log_must zfs destroy -d "$DATASET@z_middle" +snapexists "$DATASET@z_middle" || + log_fail "deferred-destroy snapshot disappeared while held" +log_must zfs bookmark "$DATASET@m_oldest" "$DATASET#normal" +log_must zfs snapshot "$DATASET@bookmark_source" +log_must zfs bookmark "$DATASET@bookmark_source" \ + "$DATASET#source_destroyed" +log_must zfs destroy "$DATASET@bookmark_source" +snapexists "$DATASET@bookmark_source" && \ + log_fail "bookmark source snapshot still exists" +verify_stale_bookmark_handles +verify_stale_snapshot_metadata +verify_projected_written +verify_unprojected_properties_use_legacy +run_injected_bookmark_errors +verify_projected_userrefs +verify_default_property_lists +verify_inapplicable_property_types +verify_colored_snapshot_listing_uses_legacy +verify_projected_sort "-s guid" +verify_projected_sort "-S guid" +verify_projected_sort "-s userrefs" +verify_projected_sort "-S userrefs" +verify_projected_sort "-s userrefs -S guid" +verify_projected_sort "-s objsetid" +verify_projected_sort "-S objsetid" +log_must set_tunable32 SNAPSHOT_LIST_BATCH_SIZE 2 +verify_filtered_batch_fill +verify_filtered_fallback +log_must set_tunable32 SNAPSHOT_LIST_BATCH_SIZE 1 +run_injected_empty_batch +run_injected_eof_after_first +log_must set_tunable32 SNAPSHOT_LIST_BATCH_SIZE 1024 +log_must zfs clone "$DATASET@m_oldest" "$CLONE_DATASET" +verify_projected_property_union +log_must zfs destroy "$CLONE_DATASET" +log_must rm -f "$EXPECTED_OUTPUT" +verify_projected_redacted +for error in cmd_unavail arg_unavail enotty enotsup enoent esrch; do + log_must snapshot_list_test callback-error "$DATASET" "$error" +done +for error in enoent esrch; do + log_must snapshot_list_test bookmark-callback-error "$DATASET" "$error" +done +run_injected_handle_enomem +verify_direct_properties +run_injected_metadata_errors +log_must eval "zfs list -H -p -t snapshot -o '$COLUMNS' '$DATASET' " \ + "> '$BATCH_OUTPUT'" +run_injected_list enomem "$INJECTED_OUTPUT" +run_injected_list real_enomem "$INJECTED_OUTPUT" +run_injected_list cmd_unavail "$INJECTED_OUTPUT" +run_injected_list enotty "$INJECTED_OUTPUT" +run_injected_list enotsup "$INJECTED_OUTPUT" +run_injected_list arg_unavail "$INJECTED_OUTPUT" +verify_mixed_type_fallback + +log_must touch "$EXPECTED_OUTPUT" +run_injected_list enoent "$INJECTED_OUTPUT" name "$EXPECTED_OUTPUT" +run_injected_list esrch "$INJECTED_OUTPUT" name "$EXPECTED_OUTPUT" + +printf "%s\n" "$DATASET@m_oldest" "$DATASET@z_middle" \ + "$DATASET@a_newest" > "$EXPECTED_OUTPUT" +run_injected_list enomem "$INJECTED_OUTPUT" name "$EXPECTED_OUTPUT" \ + "-s type" +run_injected_list cmd_unavail "$INJECTED_OUTPUT" name "$EXPECTED_OUTPUT" \ + "-s type" +run_injected_list enotty "$INJECTED_OUTPUT" name "$EXPECTED_OUTPUT" \ + "-s type" +run_injected_list enotsup "$INJECTED_OUTPUT" name "$EXPECTED_OUTPUT" \ + "-s type" +run_injected_list arg_unavail "$INJECTED_OUTPUT" name "$EXPECTED_OUTPUT" \ + "-s type" + +run_injected_interrupt +log_must set_tunable32 SNAPSHOT_LIST_BATCH_SIZE 1 +run_injected_late_failure +log_must set_tunable32 SNAPSHOT_LIST_BATCH_SIZE 1024 + +log_pass "Projected listing preserves ordering through error paths." diff --git a/tests/zfs-tests/tests/functional/cli_root/zfs_list/zfs_list_013_pos.ksh b/tests/zfs-tests/tests/functional/cli_root/zfs_list/zfs_list_013_pos.ksh new file mode 100755 index 000000000000..267cba7ff554 --- /dev/null +++ b/tests/zfs-tests/tests/functional/cli_root/zfs_list/zfs_list_013_pos.ksh @@ -0,0 +1,358 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# + +. $STF_SUITE/include/libtest.shlib + +# +# DESCRIPTION: +# Snapshot and bookmark listing remains consistent across dataset types, +# object counts, names, properties, ordering, and concurrent mutation. +# +# STRATEGY: +# 1. Compare listing output with the full-stat path for filesystems, clones, +# volumes, snapshots, bookmarks, sort keys, and JSON output. +# 2. Verify empty output and exact counts at 1023, 1024, and 1025 snapshots. +# 3. Verify maximum-length snapshot and bookmark names. +# 4. Verify default, tied, explicit-name, and mixed-type ordering. +# 5. Compare all 255 nonempty subsets of eight common properties with full +# stats. +# 6. List snapshots while another process creates, renames, and destroys one. +# + +verify_runnable "global" +set -o pipefail + +DATASET="$TESTPOOL/$TESTFS/list_generic" +CLONE="$TESTPOOL/$TESTFS/list_generic_clone" +VOLUME="$TESTPOOL/$TESTFS/list_generic_vol" +BOUNDARY_DATASET="$TESTPOOL/$TESTFS/list_generic_boundary" +SUBSET_DATASET="$TESTPOOL/$TESTFS/list_generic_subsets" +MAX_DATASET="$TESTPOOL/$TESTFS/list_generic_max_name" +DATASET_MOUNT="$TESTDIR/list_generic" +CLONE_MOUNT="$TESTDIR/list_generic_clone" +OUTPUT="$TEST_BASE_DIR/list_generic_output.$$" +LEGACY_OUTPUT="$TEST_BASE_DIR/list_generic_legacy.$$" +EXPECTED_OUTPUT="$TEST_BASE_DIR/list_generic_expected.$$" +RACE_LOG="$TEST_BASE_DIR/list_generic_race.$$" +COLUMNS="createtxg,creation,guid,name,written,type,userrefs,objsetid" +PROPERTIES="createtxg creation guid name written type userrefs objsetid" +HOLD_TAG="list-generic" +race_pid="" + +function cleanup +{ + [[ -n "$race_pid" ]] && kill "$race_pid" >/dev/null 2>&1 + [[ -n "$race_pid" ]] && wait "$race_pid" >/dev/null 2>&1 + zfs release "$HOLD_TAG" "$DATASET@after_append" >/dev/null 2>&1 + zfs release "$HOLD_TAG" "$VOLUME@second" >/dev/null 2>&1 + rm -f "$OUTPUT" "$LEGACY_OUTPUT" "$EXPECTED_OUTPUT" "$RACE_LOG" + datasetexists "$CLONE" && zfs destroy -r "$CLONE" + datasetexists "$VOLUME" && zfs destroy -r "$VOLUME" + datasetexists "$DATASET" && zfs destroy -r "$DATASET" + datasetexists "$MAX_DATASET" && zfs destroy -r "$MAX_DATASET" + datasetexists "$SUBSET_DATASET" && zfs destroy -r "$SUBSET_DATASET" + datasetexists "$BOUNDARY_DATASET" && zfs destroy -r "$BOUNDARY_DATASET" +} + +function compare_full_stat +{ + typeset dataset="$1" + typeset object_types="$2" + typeset property + typeset projected_columns="used,available,referenced,refer,mountpoint" + projected_columns="$projected_columns,logicalused,lused" + projected_columns="$projected_columns,logicalreferenced,lrefer,defer_destroy" + + for property in createtxg creation guid name written type userrefs \ + objsetid; do + log_must eval "zfs list -H -p -t '$object_types' -d 1 " \ + "-s '$property' -o '$COLUMNS' '$dataset' > '$OUTPUT'" + log_must eval "zfs list -H -p -t '$object_types' -d 1 " \ + "-s '$property' -s test:force-legacy-iterator " \ + "-o '$COLUMNS' '$dataset' > '$LEGACY_OUTPUT'" + log_must diff "$LEGACY_OUTPUT" "$OUTPUT" + done + + log_must eval "zfs list -j -p -t '$object_types' -d 1 " \ + "-o '$COLUMNS' '$dataset' > '$OUTPUT'" + log_must eval "zfs list -j -p -t '$object_types' -d 1 " \ + "-s test:force-legacy-iterator " \ + "-o '$COLUMNS' '$dataset' > '$LEGACY_OUTPUT'" + log_must diff "$LEGACY_OUTPUT" "$OUTPUT" + + log_must eval "zfs list -t '$object_types' -d 1 -o '$COLUMNS' " \ + "'$dataset' > '$OUTPUT'" + log_must eval "zfs list -t '$object_types' -d 1 " \ + "-s test:force-legacy-iterator " \ + "-o '$COLUMNS' '$dataset' > '$LEGACY_OUTPUT'" + log_must diff "$LEGACY_OUTPUT" "$OUTPUT" + + log_must eval "zfs list -H -p -t '$object_types' -d 1 " \ + "-o '$projected_columns' '$dataset' > '$OUTPUT'" + log_must eval "zfs list -H -p -t '$object_types' -d 1 " \ + "-s test:force-legacy-iterator -o '$projected_columns' '$dataset' " \ + "> '$LEGACY_OUTPUT'" + log_must diff "$LEGACY_OUTPUT" "$OUTPUT" +} + +function compare_written +{ + typeset dataset="$1" + typeset sort_options + + for sort_options in "" "-s written" "-S written"; do + log_must eval "zfs list -H -p -t snapshot -o name,written " \ + "$sort_options '$dataset' > '$OUTPUT'" + log_must eval "zfs list -H -p -t snapshot " \ + "-o name,written $sort_options " \ + "-s test:force-legacy-iterator '$dataset' " \ + "> '$LEGACY_OUTPUT'" + log_must diff "$LEGACY_OUTPUT" "$OUTPUT" + done +} + +function compare_json_name +{ + typeset dataset="$1" + typeset object_types="$2" + + log_must eval "zfs list -j -p -t '$object_types' -d 1 -o name " \ + "'$dataset' > '$OUTPUT'" + log_must eval "zfs list -j -p -t '$object_types' -d 1 " \ + "-s test:force-legacy-iterator " \ + "-o name '$dataset' > '$LEGACY_OUTPUT'" + log_must diff "$LEGACY_OUTPUT" "$OUTPUT" + + log_must eval "zfs list -j --json-int -t '$object_types' -d 1 " \ + "-o name '$dataset' > '$OUTPUT'" + log_must eval "zfs list -j --json-int -t '$object_types' -d 1 " \ + "-s test:force-legacy-iterator -o name '$dataset' " \ + "> '$LEGACY_OUTPUT'" + log_must diff "$LEGACY_OUTPUT" "$OUTPUT" +} + +function verify_count +{ + typeset expected="$1" + typeset actual + + actual=$(zfs list -H -t snapshot -o name "$BOUNDARY_DATASET" | wc -l) || + log_fail "failed to list snapshots for $BOUNDARY_DATASET" + (( actual == expected )) || + log_fail "expected $expected snapshots, found $actual" +} + +function compare_order +{ + typeset types="$1" + typeset sort_options="$2" + + log_must eval "zfs list -H -p -t '$types' -o name $sort_options " \ + "'$SUBSET_DATASET' > '$OUTPUT'" + log_must diff "$EXPECTED_OUTPUT" "$OUTPUT" + log_must eval "zfs list -H -p -t '$types' -o name $sort_options " \ + "-s test:force-legacy-iterator '$SUBSET_DATASET' " \ + "> '$LEGACY_OUTPUT'" + log_must diff "$EXPECTED_OUTPUT" "$LEGACY_OUTPUT" +} + +function compare_subset +{ + typeset columns="$1" + + log_must eval "zfs list -H -p -t snapshot,bookmark -d 1 " \ + "-o '$columns' '$SUBSET_DATASET' > '$OUTPUT'" + log_must eval "zfs list -H -p -t snapshot,bookmark -d 1 " \ + "-s test:force-legacy-iterator -o '$columns' '$SUBSET_DATASET' " \ + "> '$LEGACY_OUTPUT'" + log_must diff "$LEGACY_OUTPUT" "$OUTPUT" +} + +log_onexit cleanup +log_assert "Snapshot and bookmark listing handles generic listing cases." + +log_must zfs create "$DATASET" +log_must file_write -o create -f "$DATASET_MOUNT/payload" \ + -b 131072 -c 8 -d R +log_must zfs snapshot "$DATASET@base" +log_must file_write -o append -f "$DATASET_MOUNT/payload" \ + -b 131072 -c 4 -d R +log_must zfs snapshot "$DATASET@after_append" +log_must rm "$DATASET_MOUNT/payload" +log_must zfs snapshot "$DATASET@after_remove" +log_must file_write -o create -f "$DATASET_MOUNT/merge_payload" \ + -b 131072 -c 4 -d R +log_must zfs snapshot "$DATASET@written_before" +log_must file_write -o append -f "$DATASET_MOUNT/merge_payload" \ + -b 131072 -c 4 -d R +log_must zfs snapshot "$DATASET@written_middle" +log_must file_write -o append -f "$DATASET_MOUNT/merge_payload" \ + -b 131072 -c 4 -d R +log_must zfs snapshot "$DATASET@written_after" +compare_written "$DATASET" +log_must zfs destroy "$DATASET@written_middle" +snapexists "$DATASET@written_middle" && \ + log_fail "middle snapshot still exists after destruction" +compare_written "$DATASET" +log_must zfs hold "$HOLD_TAG" "$DATASET@after_append" +log_must zfs bookmark "$DATASET@base" "$DATASET#base" +log_must zfs bookmark "$DATASET@after_append" "$DATASET#after_append" +log_must zfs snapshot "$DATASET@bookmark_source" +log_must zfs bookmark "$DATASET@bookmark_source" \ + "$DATASET#source_destroyed" +log_must zfs destroy "$DATASET@bookmark_source" +snapexists "$DATASET@bookmark_source" && + log_fail "bookmark source snapshot still exists" + +log_must zfs clone -o mountpoint="$CLONE_MOUNT" "$DATASET@base" "$CLONE" +log_must file_write -o create -f "$CLONE_MOUNT/payload" \ + -b 131072 -c 4 -d R +log_must zfs snapshot "$CLONE@first" +log_must rm "$CLONE_MOUNT/payload" +log_must zfs snapshot "$CLONE@after_remove" +log_must zfs bookmark "$CLONE@first" "$CLONE#first" + +log_must zfs create -V 64M "$VOLUME" +log_must zfs snapshot "$VOLUME@first" +log_must zfs snapshot "$VOLUME@second" +log_must zfs hold "$HOLD_TAG" "$VOLUME@second" +log_must zfs bookmark "$VOLUME@first" "$VOLUME#first" + +compare_full_stat "$DATASET" snapshot +compare_full_stat "$DATASET" snapshot,bookmark +compare_full_stat "$CLONE" snapshot +compare_full_stat "$CLONE" snapshot,bookmark +compare_full_stat "$VOLUME" snapshot +compare_full_stat "$VOLUME" snapshot,bookmark +compare_full_stat "$DATASET" bookmark +compare_json_name "$DATASET" bookmark +compare_json_name "$DATASET" snapshot,bookmark + +log_must zfs create "$BOUNDARY_DATASET" +log_must eval "zfs list -H -p -t snapshot,bookmark -d 1 -o name " \ + "'$BOUNDARY_DATASET' > '$OUTPUT'" +[[ ! -s "$OUTPUT" ]] || log_fail "empty dataset produced objects" + +typeset -i index=0 +while (( index < 1023 )); do + suffix=$(printf "%04d" "$index") + log_must zfs snapshot "$BOUNDARY_DATASET@suffix_$suffix" + (( index += 1 )) +done +verify_count 1023 +log_must zfs snapshot "$BOUNDARY_DATASET@suffix_1023" +verify_count 1024 +log_must zfs snapshot "$BOUNDARY_DATASET@suffix_1024" +verify_count 1025 + +log_must eval "zfs list -H -p -t snapshot -o '$COLUMNS' " \ + "'$BOUNDARY_DATASET' > '$OUTPUT'" +log_must eval "zfs list -H -p -t snapshot " \ + "-s test:force-legacy-iterator -o '$COLUMNS' '$BOUNDARY_DATASET' " \ + "> '$LEGACY_OUTPUT'" +log_must diff "$LEGACY_OUTPUT" "$OUTPUT" + +log_must zfs create "$MAX_DATASET" +typeset -i max_component_length +(( max_component_length = 255 - ${#MAX_DATASET} - 1 )) +max_name=$(printf "%${max_component_length}s" "" | tr ' ' s) +too_long_name="${max_name}s" +full_name="$MAX_DATASET@$max_name" +(( ${#full_name} == 255 )) || + log_fail "failed to construct a 255-byte snapshot name" +log_must zfs snapshot "$full_name" +log_must zfs bookmark "$full_name" "$MAX_DATASET#$max_name" +log_mustnot zfs snapshot "$MAX_DATASET@$too_long_name" +log_must eval "zfs list -H -t snapshot,bookmark -d 1 -o name " \ + "'$MAX_DATASET' > '$OUTPUT'" +log_must grep -Fx "$full_name" "$OUTPUT" +log_must grep -Fx "$MAX_DATASET#$max_name" "$OUTPUT" + +log_must zfs create "$SUBSET_DATASET" +log_must zfs snapshot "$SUBSET_DATASET@m_oldest" +log_must sleep 1.1 +log_must zfs snapshot "$SUBSET_DATASET@z_middle" +log_must sleep 1.1 +log_must zfs snapshot "$SUBSET_DATASET@a_newest" +oldest_txg=$(zfs get -H -p -o value createtxg \ + "$SUBSET_DATASET@m_oldest") +middle_txg=$(zfs get -H -p -o value createtxg \ + "$SUBSET_DATASET@z_middle") +newest_txg=$(zfs get -H -p -o value createtxg \ + "$SUBSET_DATASET@a_newest") +(( oldest_txg < middle_txg && middle_txg < newest_txg )) || + log_fail "ordering snapshots do not have increasing creation TXGs" + +log_must zfs bookmark "$SUBSET_DATASET@m_oldest" \ + "$SUBSET_DATASET#m_oldest" +log_must zfs bookmark "$SUBSET_DATASET@z_middle" \ + "$SUBSET_DATASET#z_middle" +log_must zfs bookmark "$SUBSET_DATASET@a_newest" \ + "$SUBSET_DATASET#a_newest" + +printf "%s\n" "$SUBSET_DATASET@m_oldest" "$SUBSET_DATASET@z_middle" \ + "$SUBSET_DATASET@a_newest" > "$EXPECTED_OUTPUT" +compare_order snapshot "" +compare_order snapshot "-s creation" +compare_order snapshot "-s type" +compare_order snapshot "-S type" + +printf "%s\n" "$SUBSET_DATASET@a_newest" "$SUBSET_DATASET@z_middle" \ + "$SUBSET_DATASET@m_oldest" > "$EXPECTED_OUTPUT" +compare_order snapshot "-S creation" + +printf "%s\n" "$SUBSET_DATASET@a_newest" "$SUBSET_DATASET@m_oldest" \ + "$SUBSET_DATASET@z_middle" > "$EXPECTED_OUTPUT" +compare_order snapshot "-s name" + +printf "%s\n" "$SUBSET_DATASET@z_middle" "$SUBSET_DATASET@m_oldest" \ + "$SUBSET_DATASET@a_newest" > "$EXPECTED_OUTPUT" +compare_order snapshot "-S name" + +printf "%s\n" "$SUBSET_DATASET@m_oldest" "$SUBSET_DATASET@z_middle" \ + "$SUBSET_DATASET@a_newest" "$SUBSET_DATASET#a_newest" \ + "$SUBSET_DATASET#m_oldest" "$SUBSET_DATASET#z_middle" \ + > "$EXPECTED_OUTPUT" +compare_order snapshot,bookmark "-d 1" + +set -A property_array $PROPERTIES +typeset -i mask property_index +typeset columns separator +for (( mask = 1; mask < 256; mask += 1 )); do + columns="" + separator="" + for (( property_index = 0; property_index < 8; property_index += 1 )); do + if (( mask & (1 << property_index) )); then + columns="${columns}${separator}${property_array[property_index]}" + separator="," + fi + done + compare_subset "$columns" +done + +log_must zfs destroy "$BOUNDARY_DATASET@suffix_1024" +verify_count 1024 +( + typeset -i race_index=0 + while (( race_index < 64 )); do + zfs snapshot "$BOUNDARY_DATASET@zz_race" || exit 1 + zfs rename "$BOUNDARY_DATASET@zz_race" \ + "$BOUNDARY_DATASET@zz_renamed" || exit 1 + zfs destroy "$BOUNDARY_DATASET@zz_renamed" || exit 1 + (( race_index += 1 )) + done +) >"$RACE_LOG" 2>&1 & +race_pid=$! + +for (( index = 0; index < 64; index += 1 )); do + log_must eval "zfs list -H -p -t snapshot -o '$COLUMNS' " \ + "'$BOUNDARY_DATASET' > /dev/null" +done +if ! wait "$race_pid"; then + cat "$RACE_LOG" + log_fail "concurrent snapshot mutation failed" +fi +race_pid="" + +log_pass "Snapshot and bookmark listing handles generic listing cases." diff --git a/tests/zfs-tests/tests/functional/cli_root/zfs_list/zfs_list_014_pos.ksh b/tests/zfs-tests/tests/functional/cli_root/zfs_list/zfs_list_014_pos.ksh new file mode 100755 index 000000000000..c85dd0468d11 --- /dev/null +++ b/tests/zfs-tests/tests/functional/cli_root/zfs_list/zfs_list_014_pos.ksh @@ -0,0 +1,49 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# + +. $STF_SUITE/include/libtest.shlib + +# +# DESCRIPTION: +# Dependent iteration with batching includes snapshot clones. +# +# STRATEGY: +# 1. Create three snapshots and clone one into a sibling dataset. +# 2. Verify batched dependent iteration reports every snapshot and the clone. +# 3. Verify simple batched iteration reports the same dependent datasets. +# + +verify_runnable "global" +set -o pipefail + +DATASET="$TESTPOOL/$TESTFS/list_dependents" +CLONE="$TESTPOOL/$TESTFS/list_dependents_clone" +OUTPUT="$TEST_BASE_DIR/list_dependents_output.$$" +EXPECTED_OUTPUT="$TEST_BASE_DIR/list_dependents_expected.$$" + +function cleanup +{ + rm -f "$OUTPUT" "$EXPECTED_OUTPUT" + datasetexists "$CLONE" && zfs destroy -r "$CLONE" + datasetexists "$DATASET" && zfs destroy -r "$DATASET" +} + +log_onexit cleanup +log_assert "Dependent iteration with batching discovers snapshot clones." + +log_must zfs create "$DATASET" +log_must zfs snapshot "$DATASET@m_oldest" +log_must zfs snapshot "$DATASET@z_middle" +log_must zfs snapshot "$DATASET@a_newest" +log_must zfs clone -o mountpoint=none "$DATASET@m_oldest" "$CLONE" + +printf "%s\n" "$DATASET@m_oldest" "$DATASET@z_middle" \ + "$DATASET@a_newest" "$CLONE" | sort > "$EXPECTED_OUTPUT" +log_must eval "snapshot_list_test dependents '$DATASET' | sort > '$OUTPUT'" +log_must diff "$EXPECTED_OUTPUT" "$OUTPUT" +log_must eval "snapshot_list_test dependents-simple '$DATASET' | sort " \ + "> '$OUTPUT'" +log_must diff "$EXPECTED_OUTPUT" "$OUTPUT" + +log_pass "Dependent iteration with batching discovers snapshot clones." diff --git a/tests/zfs-tests/tests/functional/cli_root/zfs_list/zfs_list_016_pos.ksh b/tests/zfs-tests/tests/functional/cli_root/zfs_list/zfs_list_016_pos.ksh new file mode 100755 index 000000000000..1ae5eb693594 --- /dev/null +++ b/tests/zfs-tests/tests/functional/cli_root/zfs_list/zfs_list_016_pos.ksh @@ -0,0 +1,55 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# + +. $STF_SUITE/include/libtest.shlib + +# +# DESCRIPTION: +# Projected written-property lookup preserves legacy error handling by failing +# on a current snapshot deadlist error while omitting written when only the +# predecessor cannot be read. TXG-filtered snapshots still access the current +# deadlist but avoid accessing the predecessor. +# +# STRATEGY: +# 1. Create and export a real file-backed pool with two snapshots. +# 2. Open it through libzpool with a helper that injects EIO for one exact MOS +# object at a time through a link-time wrapper. +# 3. Verify current-deadlist and predecessor behavior for unfiltered snapshots +# and snapshots excluded by either TXG bound. +# + +verify_runnable "global" + +typeset ERROR_POOL="${TESTPOOL}_written_errors_$$" +typeset ERROR_DATASET="$ERROR_POOL/fs" +typeset ERROR_VDEV="$TEST_BASE_DIR/zfs_list_written_errors_vdev.$$" + +function cleanup +{ + if ! poolexists "$ERROR_POOL"; then + zpool import -d "$TEST_BASE_DIR" "$ERROR_POOL" >/dev/null 2>&1 + fi + poolexists "$ERROR_POOL" && destroy_pool "$ERROR_POOL" + rm -f "$ERROR_VDEV" +} + +log_onexit cleanup +log_assert "Projected written lookup distinguishes deadlist errors." + +log_must truncate -s "$MINVDEVSIZE" "$ERROR_VDEV" +log_must zpool create -f -o cachefile=none -O mountpoint=none \ + "$ERROR_POOL" "$ERROR_VDEV" +log_must zfs create -o mountpoint=none "$ERROR_DATASET" +log_must zfs snapshot "$ERROR_DATASET@first" +log_must zfs snapshot "$ERROR_DATASET@second" +log_must zpool sync "$ERROR_POOL" +log_must zpool export "$ERROR_POOL" + +log_must snapshot_list_stats_test "$ERROR_DATASET@second" "$TEST_BASE_DIR" + +log_must zpool import -d "$TEST_BASE_DIR" "$ERROR_POOL" +log_must destroy_pool "$ERROR_POOL" +log_must rm -f "$ERROR_VDEV" + +log_pass "Projected written lookup distinguishes deadlist errors." diff --git a/tests/zfs-tests/tests/functional/cli_root/zfs_list/zfs_list_017_pos.ksh b/tests/zfs-tests/tests/functional/cli_root/zfs_list/zfs_list_017_pos.ksh new file mode 100755 index 000000000000..f6664ff9a138 --- /dev/null +++ b/tests/zfs-tests/tests/functional/cli_root/zfs_list/zfs_list_017_pos.ksh @@ -0,0 +1,150 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# + +. $STF_SUITE/include/libtest.shlib + +# +# DESCRIPTION: +# Batched snapshot iteration delivers, from the failing ioctl, results +# collected before a metadata EIO and matches legacy iteration. +# +# STRATEGY: +# 1. Create three snapshots in an isolated file-backed pool. +# 2. Inject EIO into the user-reference ZAP of a late snapshot. +# 3. Verify the failing batched ioctl itself contains snapshot results. +# 4. Verify legacy and batched iteration return the same callbacks before EIO. +# + +verify_runnable "global" +set -o pipefail + +typeset ERROR_POOL="${TESTPOOL}_partial_eio_$$" +typeset ERROR_DATASET="$ERROR_POOL/fs" +typeset ERROR_VDEV="$TEST_BASE_DIR/zfs_list_partial_eio_vdev.$$" +typeset ORDER="$TEST_BASE_DIR/zfs_list_partial_eio_order.$$" +typeset MARKER="$TEST_BASE_DIR/zfs_list_partial_eio_marker.$$" +typeset HANDLER= +typeset PRELOAD +typeset USERREFS_OBJ +typeset USERREFS_HEX +typeset TARGET +typeset -i EXPECTED_CALLBACKS + +function cleanup +{ + [[ -n "$HANDLER" ]] && zinject -c "$HANDLER" >/dev/null 2>&1 + [[ -e "$TEST_BASE_DIR/tunable-SPA_LOAD_VERIFY_METADATA" ]] && + restore_tunable SPA_LOAD_VERIFY_METADATA >/dev/null 2>&1 + [[ -e "$TEST_BASE_DIR/tunable-SNAPSHOT_LIST_BATCH_TIME_US" ]] && + restore_tunable SNAPSHOT_LIST_BATCH_TIME_US >/dev/null 2>&1 + [[ -e "$TEST_BASE_DIR/tunable-SNAPSHOT_LIST_BATCH_SIZE" ]] && + restore_tunable SNAPSHOT_LIST_BATCH_SIZE >/dev/null 2>&1 + if ! poolexists "$ERROR_POOL"; then + zpool import -d "$TEST_BASE_DIR" "$ERROR_POOL" >/dev/null 2>&1 + fi + poolexists "$ERROR_POOL" && destroy_pool "$ERROR_POOL" + rm -f "$ERROR_VDEV" "$ORDER" "$MARKER" +} + +function find_shim +{ + typeset helper helper_dir candidate + + helper=$(readlink -f "$(command -v snapshot_list_test)") + helper_dir=${helper%/*} + for candidate in \ + "$helper_dir/.libs/libsnapshot_list_test_shim.so" \ + "$helper_dir/libsnapshot_list_test_shim.so" \ + "$STF_SUITE/bin/libsnapshot_list_test_shim.so"; do + [[ -f "$candidate" ]] && print -- "$candidate" && return 0 + done + return 1 +} + +function verify_partial_error +{ + typeset mode="$1" + typeset injected + + HANDLER=$(zinject -q -a -u -b "0:$USERREFS_HEX:0:0" \ + "$ERROR_POOL") || log_fail "cannot inject $mode iterator EIO" + [[ "$HANDLER" == +([0-9]) ]] || + log_fail "invalid zinject handler: $HANDLER" + + if [[ "$mode" == "batched" ]]; then + log_must rm -f "$MARKER" + log_must env LD_PRELOAD="$PRELOAD" \ + ZFS_SNAPSHOT_LIST_TEST_MODE=partial_eio_output \ + ZFS_SNAPSHOT_LIST_TEST_MARKER="$MARKER" \ + snapshot_list_test partial-error "$ERROR_DATASET" "$mode" \ + "$EXPECTED_CALLBACKS" + log_must grep -Fxq partial_eio_output "$MARKER" + else + log_must snapshot_list_test partial-error "$ERROR_DATASET" \ + "$mode" "$EXPECTED_CALLBACKS" + fi + injected=$(zinject | awk -v id="$HANDLER" \ + '$1 == id { print $NF }') + [[ -n "$injected" ]] && ((injected > 0)) || + log_fail "$mode iterator did not trigger EIO" + + log_must zinject -c "$HANDLER" + HANDLER= +} + +log_onexit cleanup +log_assert "Batched iteration returns snapshots collected before EIO." + +SHIM=$(find_shim) || log_unsupported "snapshot-list test shim not found" +PRELOAD="$SHIM" +[[ -n "$LD_PRELOAD" ]] && PRELOAD="$SHIM:$LD_PRELOAD" + +log_must save_tunable SPA_LOAD_VERIFY_METADATA +log_must save_tunable SNAPSHOT_LIST_BATCH_SIZE +log_must save_tunable SNAPSHOT_LIST_BATCH_TIME_US +log_must set_tunable32 SNAPSHOT_LIST_BATCH_SIZE 1024 +log_must set_tunable32 SNAPSHOT_LIST_BATCH_TIME_US 100000 +log_must truncate -s "$MINVDEVSIZE" "$ERROR_VDEV" +log_must zpool create -f -o cachefile=none -O mountpoint=none \ + "$ERROR_POOL" "$ERROR_VDEV" +log_must zfs create -o mountpoint=none "$ERROR_DATASET" +log_must zfs snapshot "$ERROR_DATASET@first" +log_must zfs snapshot "$ERROR_DATASET@second" +log_must zfs snapshot "$ERROR_DATASET@latest" +log_must eval "snapshot_list_test filter '$ERROR_DATASET' 0 0 > '$ORDER'" + +TARGET=$(awk -v latest="$ERROR_DATASET@latest" \ + '$0 != latest { target=$0 } END { print target }' "$ORDER") +EXPECTED_CALLBACKS=$(awk -v target="$TARGET" \ + '$0 == target { print NR - 1; exit }' "$ORDER") +[[ -n "$TARGET" ]] && ((EXPECTED_CALLBACKS > 0)) || + log_fail "cannot select a late non-latest snapshot" + +typeset -i i=0 +while ((i < 40)); do + log_must zfs hold "partial-eio-$i-abcdefghijklmnopqrstuvwxyz" "$TARGET" + ((i += 1)) +done +log_must zpool sync "$ERROR_POOL" + +typeset dsobj=$(zfs get -H -o value objsetid "$TARGET") +log_must zpool export "$ERROR_POOL" +USERREFS_OBJ=$(zdb -e -p "$TEST_BASE_DIR" -dddd "$ERROR_POOL" \ + "$dsobj" 2>/dev/null | awk '$1 == "userrefs_obj" { print $3 }') +[[ -n "$USERREFS_OBJ" ]] && ((USERREFS_OBJ > 0)) || + log_fail "cannot find user-reference ZAP for $TARGET" +USERREFS_HEX=$(printf "%x" "$USERREFS_OBJ") +log_must zpool import -d "$TEST_BASE_DIR" "$ERROR_POOL" + +log_must set_tunable32 SPA_LOAD_VERIFY_METADATA 0 +verify_partial_error legacy +verify_partial_error batched +log_must restore_tunable SPA_LOAD_VERIFY_METADATA +log_must restore_tunable SNAPSHOT_LIST_BATCH_TIME_US +log_must restore_tunable SNAPSHOT_LIST_BATCH_SIZE + +log_must destroy_pool "$ERROR_POOL" +log_must rm -f "$ERROR_VDEV" "$ORDER" "$MARKER" + +log_pass "Batched iteration returns snapshots collected before EIO."