zed: let autoexpand see capacity changes on partitioned disks

Growing a disk under a whole-disk vdev never triggers autoexpand
(#12505).  The kernel reports a capacity change on the disk itself
and nothing for the partitions, whose sizes did not change.  But
since zfs owns the whole disk it carries a partition table, and
zed_udev_monitor() drops any disk-with-partitions event on the
assumption that a partition event will follow.  For a resize none
ever does, so the ESC_DEV_DLE event that zfsdle_vdev_online() needs
is never generated and the pool stays at the old size until someone
runs zpool online -e by hand.  This is the common case for cloud
disks grown online.

Pass change events through when udev marks them RESIZE=1.  On the
matching side a disk-level event has no vdev guid to search by (the
label lives on the partition), and udev provides no ID_PATH on some
buses, so the physical path lookup can also come up empty.  When
that happens, read the ZFS label off the whole-disk partition and
match by the pool and vdev guids stored in it.  Unlike matching the
config path textually, this works no matter which name the pool was
imported with (by-id, by-path or a bare device node), and a stale
device path in an unrelated pool's config cannot steal the event,
since the label names the owning pool.  The fallback only runs for
guid-less disk events and only accepts a whole-disk vdev that is
not a spare or l2cache device.

The new zpool_expand_006_pos test covers this end to end on a
scsi_debug disk.  zpool_expand_001_pos already grows a scsi_debug
disk the same way but keeps passing on an unpatched zed, because
block_device_wait issues a bare udevadm trigger, which re-sends
change events for the zfs_member partitions and hands zed the vdev
guid the resize itself never delivered.  The new test drains the
pool-creation udev traffic and then only settles, so zed sees what
a production resize generates: one RESIZE=1 change event on the
disk.

Multipath maps take a different path through zed_udev_monitor() and
still need the manual online; that is unchanged here, and the same
goes for other device-mapper vdevs, whose partitions live on
separate dm nodes the fallback cannot derive from the map's name.
Disks with no devid source at all (virtio-blk, Xen) also stay out
of scope: their events are dropped earlier for lack of any device
identifier, and widening that is its own discussion.

Tests: zpool_expand_006_pos fails against unpatched zed and passes
with the fix; a pool imported by /dev/disk/by-id expands from the
bare disk event with "matched vdev ... by the label" in the zed
log; the zpool_expand group passes.

Reviewed-by: Brian Behlendorf <[email protected]>
Signed-off-by: MorganaFuture <[email protected]>
Closes #12505
Closes #18833
This commit is contained in:
MorganaFuture authored and GitHub committed 2026-07-27 15:15:49 -07:00
1 parent a8458ef364
commit 466d90e15d
5 files changed
+179 -3

No files matched your search

+57 -2
View File
@@ -1080,8 +1080,63 @@ zfsdle_vdev_online(zpool_handle_t *zhp, void *data)
zed_log_msg(LOG_INFO, "zfsdle_vdev_online: searching for '%s' in '%s'",
devname, zpool_get_name(zhp));
if ((tgt = zpool_find_vdev_by_physpath(zhp, devname,
&avail_spare, &l2cache, NULL)) != NULL) {
tgt = zpool_find_vdev_by_physpath(zhp, devname,
&avail_spare, &l2cache, NULL);
/*
* A whole-disk event carries no vdev guid (the label lives on
* the partition), and udev provides no ID_PATH on some buses,
* so neither lookup above can match. Identify the vdev by
* reading the label off the whole-disk partition instead: the
* pool and vdev guids stored there don't depend on which name
* the pool was imported with (by-id, by-path or a bare device
* node), and they can't be forged by a stale config path left
* behind in an unrelated pool.
*/
if (tgt == NULL && nvlist_lookup_uint64(udev_nvl, ZFS_EV_VDEV_GUID,
&guid) != 0 && nvlist_lookup_string(udev_nvl, DEV_NAME,
&tmp_devname) == 0 && !nvlist_exists(udev_nvl, DEV_IS_PART)) {
nvlist_t *label = NULL;
uint64_t label_pool_guid = 0, label_vdev_guid = 0;
int fd;
strlcpy(devname, tmp_devname, MAXPATHLEN);
zfs_append_partition(devname, MAXPATHLEN);
if ((fd = open(devname, O_RDONLY | O_CLOEXEC)) >= 0) {
if (zpool_read_label(fd, &label, NULL) == 0 &&
label != NULL &&
nvlist_lookup_uint64(label,
ZPOOL_CONFIG_POOL_GUID, &label_pool_guid) == 0 &&
nvlist_lookup_uint64(label, ZPOOL_CONFIG_GUID,
&label_vdev_guid) == 0 &&
label_pool_guid == zpool_get_prop_int(zhp,
ZPOOL_PROP_GUID, NULL)) {
zed_log_msg(LOG_INFO, "zfsdle_vdev_online: "
"matched vdev %llu by the label on '%s'",
(u_longlong_t)label_vdev_guid, devname);
(void) snprintf(devname, MAXPATHLEN, "%llu",
(u_longlong_t)label_vdev_guid);
tgt = zpool_find_vdev(zhp, devname,
&avail_spare, &l2cache, NULL);
if (tgt != NULL && (avail_spare || l2cache))
tgt = NULL;
if (tgt != NULL) {
uint64_t wd = 0;
(void) nvlist_lookup_uint64(tgt,
ZPOOL_CONFIG_WHOLE_DISK, &wd);
if (!wd)
tgt = NULL;
}
}
nvlist_free(label);
(void) close(fd);
}
}
if (tgt != NULL) {
const char *path;
char fullpath[MAXPATHLEN];
uint64_t wholedisk = 0;
+16
View File
@@ -242,6 +242,9 @@ zed_udev_monitor(void *arg)
part != NULL && part[0] != '\0') {
const char *devname =
udev_device_get_property_value(dev, "DEVNAME");
const char *resize =
udev_device_get_property_value(dev, "RESIZE");
const char *dev_action = udev_device_get_action(dev);
if (strcmp(part, "atari") == 0) {
zed_log_msg(LOG_INFO,
@@ -249,6 +252,19 @@ zed_udev_monitor(void *arg)
"but we're going to assume it's a false "
"positive and still use it (issue #13497)",
__func__, devname);
} else if (dev_action != NULL &&
strcmp(dev_action, "change") == 0 &&
resize != NULL && strcmp(resize, "1") == 0) {
/*
* A capacity change is reported on the disk
* itself, not on its partitions, so this is
* the only chance to notice it. Pass the
* event on for a possible autoexpand of a
* whole-disk vdev (issue #12505).
*/
zed_log_msg(LOG_INFO,
"%s: %s capacity change on partitioned "
"disk", __func__, devname);
} else {
zed_log_msg(LOG_INFO,
"%s: skip %s since it has a %s partition "
+2 -1
View File
@@ -70,7 +70,8 @@ tags = ['functional', 'cli_root', 'zpool_add']
[tests/functional/cli_root/zpool_expand:Linux]
tests = ['zpool_expand_001_pos', 'zpool_expand_002_pos',
'zpool_expand_003_neg', 'zpool_expand_004_pos', 'zpool_expand_005_pos']
'zpool_expand_003_neg', 'zpool_expand_004_pos', 'zpool_expand_005_pos',
'zpool_expand_006_pos']
tags = ['functional', 'cli_root', 'zpool_expand']
[tests/functional/cli_root/zpool_import:Linux]
+1
View File
@@ -1185,6 +1185,7 @@ nobase_dist_datadir_zfs_tests_tests_SCRIPTS += \
functional/cli_root/zpool_expand/zpool_expand_003_neg.ksh \
functional/cli_root/zpool_expand/zpool_expand_004_pos.ksh \
functional/cli_root/zpool_expand/zpool_expand_005_pos.ksh \
functional/cli_root/zpool_expand/zpool_expand_006_pos.ksh \
functional/cli_root/zpool_export/cleanup.ksh \
functional/cli_root/zpool_export/setup.ksh \
functional/cli_root/zpool_export/zpool_export_001_pos.ksh \
@@ -0,0 +1,103 @@
#!/bin/ksh -p
# SPDX-License-Identifier: CDDL-1.0
#
# CDDL HEADER START
#
# 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.
#
# CDDL HEADER END
#
#
# Copyright (c) 2026 by Andrew Mochalskyi. All rights reserved.
#
. $STF_SUITE/include/libtest.shlib
. $STF_SUITE/tests/functional/cli_root/zpool_expand/zpool_expand.cfg
#
# DESCRIPTION:
# A pool on a whole-disk vdev autoexpands from the kernel's own
# capacity change event alone (issue #12505).
#
# STRATEGY:
# 1) Create a pool with autoexpand=on on a single scsi_debug device,
# so zfs owns the whole disk and puts a partition table on it
# 2) Grow the disk and let the SCSI layer pick up the new capacity
# 3) Wait for the pool to expand
#
# The kernel reports a resize with a single change event on the disk;
# nothing is generated for the partitions. zpool_expand_001_pos
# passes this point on an unpatched zed only because
# block_device_wait runs a bare udevadm trigger, which resends change
# events for the zfs_member partitions and hands zed the vdev guid
# the resize never delivered. For the same reason the udev events
# left over from pool creation must be drained before growing the
# disk: only udevadm settle may be used after that point, so that zed
# sees the resize event and nothing else, just like with a disk grown
# in production.
#
verify_runnable "global"
function cleanup
{
poolexists $TESTPOOL1 && destroy_pool $TESTPOOL1
unload_scsi_debug
}
# Wait for the pool size to grow past $1.
function wait_for_autoexpand
{
typeset prev_size=$1
typeset -i i
for (( i = 0; i < 60; i++ )); do
typeset new_size=$(get_pool_prop size $TESTPOOL1)
if [[ $new_size -gt $prev_size ]]; then
return
fi
sleep 1
done
log_fail "$TESTPOOL1 never grew past $prev_size (still $new_size)"
}
log_onexit cleanup
log_assert "whole-disk pool autoexpands from the disk's own resize event"
MINVDEVSIZE_MB=$((MINVDEVSIZE / 1048576))
load_scsi_debug $MINVDEVSIZE_MB 1 1 1 '512b'
block_device_wait
DEV=$(get_debug_device)
log_must zpool create -o autoexpand=on $TESTPOOL1 $DEV
# Let the udev fallout of the pool creation (partition table writes,
# device node closes) fully play out before the resize, so the only
# event left for zed to see below is the resize itself.
udevadm settle
sleep 2
udevadm settle
typeset prev_size=$(get_pool_prop size $TESTPOOL1)
# Grow the device and have the SCSI disk driver re-read the capacity.
# This makes the kernel emit a change event with RESIZE=1 for the
# disk, and nothing for its partitions.
echo "2" > /sys/bus/pseudo/drivers/scsi_debug/virtual_gb
echo "1" > /sys/class/block/$DEV/device/rescan
udevadm settle
wait_for_autoexpand $prev_size
log_note "$TESTPOOL1 grew from $prev_size to $(get_pool_prop size $TESTPOOL1)"
log_pass "whole-disk pool autoexpanded from the disk's own resize event"