diff --git a/cmd/zed/agents/zfs_retire.c b/cmd/zed/agents/zfs_retire.c index 8aabf6d3bf7..0c6c30f2e86 100644 --- a/cmd/zed/agents/zfs_retire.c +++ b/cmd/zed/agents/zfs_retire.c @@ -350,6 +350,47 @@ is_draid_fdomain_failure(fmd_hdl_t *hdl, libzfs_handle_t *zhdl, return (res); } +/* + * Returns B_TRUE if spare 'a' should be tried before spare 'b' when + * replacing a failed vdev with the given characteristics. + * + * Ordering criteria (most to least significant): + * 1. Matching rotational is preferred over mismatching. + * 2. Large enough is preferred over (potentially?) too small. + * 3. Smaller size is preferred over bigger (best fit). + */ +static boolean_t +spare_is_preferred(nvlist_t *a, nvlist_t *b, boolean_t have_rotational, + uint64_t vdev_rotational, uint64_t vdev_size) +{ + uint64_t a_rotational = 0, b_rotational = 0; + uint64_t a_size = 0, b_size = 0; + + if (have_rotational) { + (void) nvlist_lookup_uint64(a, ZPOOL_CONFIG_VDEV_ROTATIONAL, + &a_rotational); + (void) nvlist_lookup_uint64(b, ZPOOL_CONFIG_VDEV_ROTATIONAL, + &b_rotational); + if ((a_rotational == vdev_rotational) != + (b_rotational == vdev_rotational)) + return (a_rotational == vdev_rotational); + } + + vdev_stat_t *vs; + unsigned int c; + if (nvlist_lookup_uint64_array(a, ZPOOL_CONFIG_VDEV_STATS, + (uint64_t **)&vs, &c) == 0) + a_size = vs->vs_rsize; + if (nvlist_lookup_uint64_array(b, ZPOOL_CONFIG_VDEV_STATS, + (uint64_t **)&vs, &c) == 0) + b_size = vs->vs_rsize; + boolean_t a_ok = (a_size >= vdev_size); + boolean_t b_ok = (b_size >= vdev_size); + if (a_ok != b_ok) + return (a_ok); + return (a_size < b_size); +} + /* * Given a vdev, attempt to replace it with every known spare until one * succeeds or we run out of devices to try. @@ -364,6 +405,10 @@ replace_with_spare(fmd_hdl_t *hdl, zpool_handle_t *zhp, nvlist_t *vdev) char *dev_name; zprop_source_t source; int ashift; + uint64_t vdev_rotational = 0, vdev_size = 0; + boolean_t have_vdev_rotational; + vdev_stat_t *vs; + unsigned int c; config = zpool_get_config(zhp, NULL); if (nvlist_lookup_nvlist(config, ZPOOL_CONFIG_VDEV_TREE, @@ -377,6 +422,34 @@ replace_with_spare(fmd_hdl_t *hdl, zpool_handle_t *zhp, nvlist_t *vdev) &spares, &nspares) != 0) return (B_FALSE); + /* + * Collect the failed vdev's parameters for optimal replacement. + */ + have_vdev_rotational = (nvlist_lookup_uint64(vdev, + ZPOOL_CONFIG_VDEV_ROTATIONAL, &vdev_rotational) == 0); + if (nvlist_lookup_uint64_array(vdev, ZPOOL_CONFIG_VDEV_STATS, + (uint64_t **)&vs, &c) == 0) + vdev_size = vs->vs_rsize; + + /* + * Build a sorted index array over the spares, so that better + * candicates are tried first. + */ + uint_t order[nspares]; + for (s = 0; s < nspares; s++) + order[s] = s; + for (s = 1; s < nspares; s++) { + uint_t key = order[s]; + int j = (int)s - 1; + while (j >= 0 && spare_is_preferred(spares[key], + spares[order[j]], have_vdev_rotational, vdev_rotational, + vdev_size)) { + order[j + 1] = order[j]; + j--; + } + order[j + 1] = key; + } + /* * lookup "ashift" pool property, we may need it for the replacement */ @@ -394,25 +467,26 @@ replace_with_spare(fmd_hdl_t *hdl, zpool_handle_t *zhp, nvlist_t *vdev) * replace it. */ for (s = 0; s < nspares; s++) { + nvlist_t *spare = spares[order[s]]; boolean_t rebuild = B_FALSE; const char *spare_name, *type; - if (nvlist_lookup_string(spares[s], ZPOOL_CONFIG_PATH, + if (nvlist_lookup_string(spare, ZPOOL_CONFIG_PATH, &spare_name) != 0) continue; /* prefer sequential resilvering for distributed spares */ - if ((nvlist_lookup_string(spares[s], ZPOOL_CONFIG_TYPE, + if ((nvlist_lookup_string(spare, ZPOOL_CONFIG_TYPE, &type) == 0) && strcmp(type, VDEV_TYPE_DRAID_SPARE) == 0) rebuild = B_TRUE; /* if set, add the "ashift" pool property to the spare nvlist */ if (source != ZPROP_SRC_DEFAULT) - (void) nvlist_add_uint64(spares[s], + (void) nvlist_add_uint64(spare, ZPOOL_CONFIG_ASHIFT, ashift); (void) nvlist_add_nvlist_array(replacement, - ZPOOL_CONFIG_CHILDREN, (const nvlist_t **)&spares[s], 1); + ZPOOL_CONFIG_CHILDREN, (const nvlist_t **)&spare, 1); fmd_hdl_debug(hdl, "zpool_vdev_replace '%s' with spare '%s'", dev_name, zfs_basename(spare_name)); diff --git a/include/sys/fs/zfs.h b/include/sys/fs/zfs.h index 4c4d15f8ce0..8e877166ada 100644 --- a/include/sys/fs/zfs.h +++ b/include/sys/fs/zfs.h @@ -478,6 +478,7 @@ typedef enum { VDEV_PROP_FDOMAIN, VDEV_PROP_FGROUP, VDEV_PROP_ALLOC_BIAS, + VDEV_PROP_ROTATIONAL, VDEV_NUM_PROPS } vdev_prop_t; @@ -931,6 +932,7 @@ typedef struct zpool_load_policy { #define ZPOOL_CONFIG_VDEV_ENC_SYSFS_PATH "vdev_enc_sysfs_path" #define ZPOOL_CONFIG_WHOLE_DISK "whole_disk" +#define ZPOOL_CONFIG_VDEV_ROTATIONAL "rotational" #define ZPOOL_CONFIG_ERRCOUNT "error_count" #define ZPOOL_CONFIG_NOT_PRESENT "not_present" #define ZPOOL_CONFIG_SPARES "spares" diff --git a/lib/libzfs/libzfs.abi b/lib/libzfs/libzfs.abi index be74babbcba..3f88f2fb83d 100644 --- a/lib/libzfs/libzfs.abi +++ b/lib/libzfs/libzfs.abi @@ -6416,7 +6416,8 @@ - + + diff --git a/man/man7/vdevprops.7 b/man/man7/vdevprops.7 index da38acafeee..b52c6d4b023 100644 --- a/man/man7/vdevprops.7 +++ b/man/man7/vdevprops.7 @@ -142,6 +142,8 @@ See .Xr zpool-attach 8 . .It Sy trim_support Indicates if a leaf device supports trim operations. +.It Sy rotational +Indicates whether the device backing this vdev uses rotating media. .El .Pp The following native properties can be used to change the behavior of a vdev. diff --git a/module/zcommon/zpool_prop.c b/module/zcommon/zpool_prop.c index ccd9f3854f5..09f5c88d8fb 100644 --- a/module/zcommon/zpool_prop.c +++ b/module/zcommon/zpool_prop.c @@ -574,6 +574,9 @@ vdev_prop_init(void) VDEV_BIAS_NONE, PROP_DEFAULT, ZFS_TYPE_VDEV, "none | log | special | dedup", "ALLOC_BIAS", vdev_alloc_bias_table, sfeatures); + zprop_register_index(VDEV_PROP_ROTATIONAL, "rotational", 0, + PROP_READONLY, ZFS_TYPE_VDEV, "on | off", "ROTATIONAL", + boolean_table, sfeatures); /* hidden properties */ zprop_register_hidden(VDEV_PROP_NAME, "name", PROP_TYPE_STRING, diff --git a/module/zfs/spa.c b/module/zfs/spa.c index 7c466bf2d22..ec93ce97433 100644 --- a/module/zfs/spa.c +++ b/module/zfs/spa.c @@ -8333,12 +8333,20 @@ spa_vdev_attach(spa_t *spa, uint64_t guid, nvlist_t *nvroot, int replacing, return (spa_vdev_exit(spa, newrootvd, txg, error)); /* - * log, dedup and special vdevs should not be replaced by spares. + * Spares can't replace logs */ - if ((oldvd->vdev_top->vdev_alloc_bias != VDEV_BIAS_NONE || - oldvd->vdev_top->vdev_islog) && newvd->vdev_isspare) { + if (oldvd->vdev_top->vdev_islog && newvd->vdev_isspare) + return (spa_vdev_exit(spa, newrootvd, txg, ENOTSUP)); + + /* + * For special and dedup vdevs a spare must have matching rotational + * characteristics. A rotating spare replacing a non-rotating vdev + * would silently degrade pool performance, so we reject the mismatch. + */ + if (newvd->vdev_isspare && + oldvd->vdev_top->vdev_alloc_bias != VDEV_BIAS_NONE && + newvd->vdev_nonrot != oldvd->vdev_nonrot) return (spa_vdev_exit(spa, newrootvd, txg, ENOTSUP)); - } /* * A dRAID spare can only replace a child of its parent dRAID vdev. diff --git a/module/zfs/vdev.c b/module/zfs/vdev.c index e4dc9e97af7..91cd9c6dc84 100644 --- a/module/zfs/vdev.c +++ b/module/zfs/vdev.c @@ -474,8 +474,11 @@ vdev_prop_get_int(vdev_t *vd, vdev_prop_t prop, uint64_t *value) uint64_t objid; int err; - if (vdev_prop_get_objid(vd, &objid) != 0) - return (EINVAL); + if (vdev_prop_get_objid(vd, &objid) != 0) { + /* No ZAP: property was never set, return the default. */ + *value = vdev_prop_default_numeric(prop); + return (ENOENT); + } err = zap_lookup(mos, objid, vdev_prop_to_name(prop), sizeof (uint64_t), 1, value); @@ -963,6 +966,20 @@ vdev_alloc(spa_t *spa, vdev_t **vdp, nvlist_t *nv, vdev_t *parent, uint_t id, &vd->vdev_wholedisk) != 0) vd->vdev_wholedisk = -1ULL; + /* + * Restore the last-known rotational status for leaf vdevs. vdev_open() + * will overwrite this with the hardware value when the device is + * accessible; the persisted value acts as a fallback for failed or + * missing devices so that spare selection can still match on device + * type even when the original disk is gone. + */ + if (vd->vdev_ops->vdev_op_leaf) { + uint64_t rotational = 0; + if (nvlist_lookup_uint64(nv, ZPOOL_CONFIG_VDEV_ROTATIONAL, + &rotational) == 0) + vd->vdev_nonrot = !rotational; + } + vic = &vd->vdev_indirect_config; ASSERT0(vic->vic_mapping_object); @@ -6446,9 +6463,15 @@ vdev_prop_get(vdev_t *vd, nvlist_t *innvl, nvlist_t *outnvl) nvlist_lookup_nvlist(innvl, ZPOOL_VDEV_PROPS_GET_PROPS, &nvprops); - if (vdev_prop_get_objid(vd, &objid) != 0) - return (SET_ERROR(EINVAL)); - ASSERT(objid != 0); + /* + * A missing ZAP is normal for spare and L2ARC vdevs, which are + * not part of the main vdev tree and never get ZAPs allocated. + * Many properties are sourced directly from vdev_t fields and + * work fine without one; ZAP-backed properties will return their + * default values. objid is set to 0 when absent and the few + * cases that call zap_lookup directly guard against this below. + */ + (void) vdev_prop_get_objid(vd, &objid); mutex_enter(&spa->spa_props_lock); @@ -6772,8 +6795,13 @@ vdev_prop_get(vdev_t *vd, nvlist_t *innvl, nvlist_t *outnvl) case VDEV_PROP_FAILFAST: src = ZPROP_SRC_LOCAL; - err = zap_lookup(mos, objid, nvpair_name(elem), - sizeof (uint64_t), 1, &intval); + if (objid != 0) { + err = zap_lookup(mos, objid, + nvpair_name(elem), + sizeof (uint64_t), 1, &intval); + } else { + err = ENOENT; + } if (err == ENOENT) { if (vd->vdev_ops == &vdev_root_ops) intval = @@ -6835,6 +6863,10 @@ vdev_prop_get(vdev_t *vd, nvlist_t *innvl, nvlist_t *outnvl) ZPROP_SRC_NONE); } continue; + case VDEV_PROP_ROTATIONAL: + vdev_prop_add_list(outnvl, propname, NULL, + !vd->vdev_nonrot, ZPROP_SRC_NONE); + continue; case VDEV_PROP_CHECKSUM_N: case VDEV_PROP_CHECKSUM_T: case VDEV_PROP_IO_N: @@ -6860,6 +6892,8 @@ vdev_prop_get(vdev_t *vd, nvlist_t *innvl, nvlist_t *outnvl) /* FALLTHRU */ case VDEV_PROP_USERPROP: /* User Properites */ + if (objid == 0) + continue; src = ZPROP_SRC_LOCAL; err = zap_length(mos, objid, nvpair_name(elem), diff --git a/module/zfs/vdev_label.c b/module/zfs/vdev_label.c index b1371b0349c..b3042980aad 100644 --- a/module/zfs/vdev_label.c +++ b/module/zfs/vdev_label.c @@ -493,6 +493,11 @@ vdev_config_generate(spa_t *spa, vdev_t *vd, boolean_t getstats, vd->vdev_wholedisk); } + if (vd->vdev_ops->vdev_op_leaf) { + fnvlist_add_uint64(nv, ZPOOL_CONFIG_VDEV_ROTATIONAL, + !vd->vdev_nonrot); + } + if (vd->vdev_not_present && !(flags & VDEV_CONFIG_MISSING)) fnvlist_add_uint64(nv, ZPOOL_CONFIG_NOT_PRESENT, 1); @@ -502,6 +507,9 @@ vdev_config_generate(spa_t *spa, vdev_t *vd, boolean_t getstats, if (flags & VDEV_CONFIG_L2CACHE) fnvlist_add_uint64(nv, ZPOOL_CONFIG_ASHIFT, vd->vdev_ashift); + if ((flags & VDEV_CONFIG_SPARE) && vd->vdev_asize != 0) + fnvlist_add_uint64(nv, ZPOOL_CONFIG_ASIZE, vd->vdev_asize); + if (!(flags & (VDEV_CONFIG_SPARE | VDEV_CONFIG_L2CACHE)) && vd == vd->vdev_top) { fnvlist_add_uint64(nv, ZPOOL_CONFIG_METASLAB_ARRAY, diff --git a/tests/runfiles/common.run b/tests/runfiles/common.run index 6e62b552a0d..0dda8fdfa36 100644 --- a/tests/runfiles/common.run +++ b/tests/runfiles/common.run @@ -1111,7 +1111,7 @@ tags = ['functional', 'vdev_disk'] [tests/functional/vdev_zaps] tests = ['vdev_zaps_001_pos', 'vdev_zaps_002_pos', 'vdev_zaps_003_pos', 'vdev_zaps_004_pos', 'vdev_zaps_005_pos', 'vdev_zaps_006_pos', - 'vdev_zaps_007_pos'] + 'vdev_zaps_007_pos', 'vdev_zaps_008_pos'] tags = ['functional', 'vdev_zaps'] [tests/functional/write_dirs] diff --git a/tests/runfiles/linux.run b/tests/runfiles/linux.run index 11bda60a9ca..009d984f2b9 100644 --- a/tests/runfiles/linux.run +++ b/tests/runfiles/linux.run @@ -118,7 +118,8 @@ tags = ['functional', 'fallocate'] tests = ['auto_offline_001_pos', 'auto_online_001_pos', 'auto_online_002_pos', 'auto_replace_001_pos', 'auto_replace_002_pos', 'auto_spare_001_pos', 'auto_spare_002_pos', 'auto_spare_double', 'auto_spare_multiple', - 'auto_spare_ashift', 'auto_spare_shared', 'decrypt_fault', + 'auto_spare_ashift', 'auto_spare_rotational', 'auto_spare_shared', + 'decrypt_fault', 'decompress_fault', 'fault_limits', 'scrub_after_resilver', 'suspend_on_probe_errors', 'suspend_resume_single', 'suspend_draid_fgroups', 'zpool_status_-s'] diff --git a/tests/zfs-tests/tests/Makefile.am b/tests/zfs-tests/tests/Makefile.am index 98f39253882..c7931ca95e2 100644 --- a/tests/zfs-tests/tests/Makefile.am +++ b/tests/zfs-tests/tests/Makefile.am @@ -1620,6 +1620,7 @@ nobase_dist_datadir_zfs_tests_tests_SCRIPTS += \ functional/fault/auto_spare_001_pos.ksh \ functional/fault/auto_spare_002_pos.ksh \ functional/fault/auto_spare_ashift.ksh \ + functional/fault/auto_spare_rotational.ksh \ functional/fault/auto_spare_double.ksh \ functional/fault/auto_spare_multiple.ksh \ functional/fault/auto_spare_shared.ksh \ @@ -2292,6 +2293,7 @@ nobase_dist_datadir_zfs_tests_tests_SCRIPTS += \ functional/vdev_zaps/vdev_zaps_005_pos.ksh \ functional/vdev_zaps/vdev_zaps_006_pos.ksh \ functional/vdev_zaps/vdev_zaps_007_pos.ksh \ + functional/vdev_zaps/vdev_zaps_008_pos.ksh \ functional/write_dirs/cleanup.ksh \ functional/write_dirs/setup.ksh \ functional/write_dirs/write_dirs_001_pos.ksh \ diff --git a/tests/zfs-tests/tests/functional/cli_root/zpool_get/vdev_get.cfg b/tests/zfs-tests/tests/functional/cli_root/zpool_get/vdev_get.cfg index 79992227169..be17821ba1a 100644 --- a/tests/zfs-tests/tests/functional/cli_root/zpool_get/vdev_get.cfg +++ b/tests/zfs-tests/tests/functional/cli_root/zpool_get/vdev_get.cfg @@ -66,6 +66,7 @@ typeset -a properties=( trim_bytes removing allocating + rotational failfast checksum_n checksum_t diff --git a/tests/zfs-tests/tests/functional/fault/auto_spare_rotational.ksh b/tests/zfs-tests/tests/functional/fault/auto_spare_rotational.ksh new file mode 100755 index 00000000000..5378979a8bb --- /dev/null +++ b/tests/zfs-tests/tests/functional/fault/auto_spare_rotational.ksh @@ -0,0 +1,84 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# This file and its contents are supplied under the terms of the +# Common Development and Distribution License ("CDDL"), version 1.0. +# You may only use this file in accordance with the terms of version +# 1.0 of the CDDL. +# +# A full copy of the text of the CDDL should have accompanied this +# source. A copy of the CDDL is also available via the Internet at +# http://www.illumos.org/license/CDDL. +# + +# +# Copyright (c) 2026, TrueNAS. +# + +. $STF_SUITE/include/libtest.shlib +. $STF_SUITE/tests/functional/fault/fault.cfg + +# +# DESCRIPTION: +# ZED prefers the smallest sufficient spare when replacing a faulted +# special vdev, regardless of spare list order. +# +# The 'rotational' property is persisted in the pool config for all leaf +# vdevs so that spare selection can match device type even after the +# original disk is gone. ZED sorts spares preferring matching rotational +# and, among equally-matching spares, the smallest sufficient one. +# +# STRATEGY: +# 1. Create a pool with a normal mirror, a special mirror, and two file +# spares of different sizes. List the larger spare first so that the +# sorted order contradicts the list order. +# 2. Fault a member of the special mirror; verify ZED activates the +# smaller sufficient spare, leaving the larger spare available. +# + +verify_runnable "both" + +NORM1="$TEST_BASE_DIR/rotational-norm1" +NORM2="$TEST_BASE_DIR/rotational-norm2" +SPEC1="$TEST_BASE_DIR/rotational-spec1" +SPEC2="$TEST_BASE_DIR/rotational-spec2" +SPARE_SMALL="$TEST_BASE_DIR/rotational-spare-small" +SPARE_LARGE="$TEST_BASE_DIR/rotational-spare-large" + +LARGE_SIZE=$((MINVDEVSIZE * 2)) + +function cleanup +{ + log_must zinject -c all + destroy_pool $TESTPOOL + rm -f $NORM1 $NORM2 $SPEC1 $SPEC2 $SPARE_SMALL $SPARE_LARGE +} + +log_assert "ZED selects smallest sufficient spare for a faulted special vdev" +log_onexit cleanup + +zed_events_drain + +log_must truncate -s $MINVDEVSIZE $NORM1 $NORM2 $SPEC1 $SPEC2 $SPARE_SMALL +log_must truncate -s $LARGE_SIZE $SPARE_LARGE + +# SPARE_LARGE is listed first so that size-preference sorting is what +# causes SPARE_SMALL to be selected, not merely list order. +log_must zpool create -f $TESTPOOL \ + mirror $NORM1 $NORM2 \ + special mirror $SPEC1 $SPEC2 \ + spare $SPARE_LARGE $SPARE_SMALL + +log_must zinject -d $SPEC1 -e io -T all -f 100 $TESTPOOL +log_must zpool scrub $TESTPOOL + +log_note "Wait for ZED to auto-spare the special vdev" +log_must wait_vdev_state $TESTPOOL $SPEC1 "FAULTED" 60 +log_must wait_hotspare_state $TESTPOOL $SPARE_SMALL "INUSE" + +# The larger spare must not have been activated. +log_must wait_hotspare_state $TESTPOOL $SPARE_LARGE "AVAIL" + +log_must check_state $TESTPOOL "" "DEGRADED" + +log_pass "ZED activated the smallest sufficient spare for the special vdev" diff --git a/tests/zfs-tests/tests/functional/vdev_zaps/vdev_zaps_008_pos.ksh b/tests/zfs-tests/tests/functional/vdev_zaps/vdev_zaps_008_pos.ksh new file mode 100755 index 00000000000..c5ad282eb8a --- /dev/null +++ b/tests/zfs-tests/tests/functional/vdev_zaps/vdev_zaps_008_pos.ksh @@ -0,0 +1,90 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# This file and its contents are supplied under the terms of the +# Common Development and Distribution License ("CDDL"), version 1.0. +# You may only use this file in accordance with the terms of version +# 1.0 of the CDDL. +# +# A full copy of the text of the CDDL should have accompanied this +# source. A copy of the CDDL is also available via the Internet at +# http://www.illumos.org/license/CDDL. +# + +# +# Copyright (c) 2026, TrueNAS. +# + +. $STF_SUITE/include/libtest.shlib + +# +# DESCRIPTION: +# Verify that the 'rotational' vdev property is readable on spare and +# L2ARC vdevs, which have no per-vdev ZAP, and that its value persists +# across export/import when the spare device is absent. +# +# STRATEGY: +# 1. Create a pool with a mirror, a spare, and an L2ARC device. +# 2. Verify 'rotational' is readable on leaf, virtual (mirror), spare, +# and L2ARC vdevs. +# 3. Export the pool, remove the spare file, re-import, and verify that +# 'rotational' still reports the same value for the missing spare, +# proving the value comes from the persisted config. +# + +verify_runnable "global" + +SPARE="$TEST_BASE_DIR/vz008-spare" +L2C="$TEST_BASE_DIR/vz008-l2c" +VDEV1="$TEST_BASE_DIR/vz008-vdev1" +VDEV2="$TEST_BASE_DIR/vz008-vdev2" + +function cleanup +{ + destroy_pool $TESTPOOL + rm -f $VDEV1 $VDEV2 $SPARE $L2C +} + +log_assert "'rotational' is readable on ZAP-less vdevs and persists absent" +log_onexit cleanup + +log_must truncate -s $MINVDEVSIZE $VDEV1 $VDEV2 $SPARE $L2C + +log_must zpool create -f $TESTPOOL \ + mirror $VDEV1 $VDEV2 \ + cache $L2C \ + spare $SPARE + +# Leaf vdev should report rotational. +NR=$(zpool get -H -o value rotational $TESTPOOL $VDEV1) +[[ "$NR" == "on" || "$NR" == "off" ]] || + log_fail "leaf $VDEV1: expected on/off, got '$NR'" + +# Virtual (mirror) vdev should report rotational. +MIRROR=$(zpool list -v -H $TESTPOOL | awk '$1 ~ /^mirror/ {print $1; exit}') +NR=$(zpool get -H -o value rotational $TESTPOOL "$MIRROR") +[[ "$NR" == "on" || "$NR" == "off" ]] || + log_fail "mirror: expected on/off, got '$NR'" + +# Spare vdev should report rotational even though it has no ZAP. +NR=$(zpool get -H -o value rotational $TESTPOOL $SPARE) +[[ "$NR" == "on" || "$NR" == "off" ]] || + log_fail "spare $SPARE: expected on/off, got '$NR'" + +# L2ARC vdev should report rotational even though it has no ZAP. +NR=$(zpool get -H -o value rotational $TESTPOOL $L2C) +[[ "$NR" == "on" || "$NR" == "off" ]] || + log_fail "L2ARC $L2C: expected on/off, got '$NR'" + +# The value must persist across export/import when the spare is absent. +# Remove the spare file before re-import so that vdev_open() cannot read +# the hardware value and the only source is the persisted config. +NR_BEFORE=$(zpool get -H -o value rotational $TESTPOOL $SPARE) +log_must zpool export $TESTPOOL +log_must rm -f $SPARE +log_must zpool import -d $TEST_BASE_DIR $TESTPOOL +NR_AFTER=$(zpool get -H -o value rotational $TESTPOOL $SPARE) +[[ "$NR_BEFORE" == "$NR_AFTER" ]] || + log_fail "spare rotational changed across import: $NR_BEFORE -> $NR_AFTER" + +log_pass "'rotational' readable on spare/L2ARC vdevs and persists when absent"