diff --git a/etc/mtree/BSD.include.dist b/etc/mtree/BSD.include.dist index 05e739bcf5dd..75c66beeefbe 100644 --- a/etc/mtree/BSD.include.dist +++ b/etc/mtree/BSD.include.dist @@ -333,6 +333,8 @@ .. virstor .. + zoned + .. .. gssapi tags=package=kerberos-dev .. diff --git a/etc/mtree/BSD.tests.dist b/etc/mtree/BSD.tests.dist index 3843ac9254b5..20f3b36ab40b 100644 --- a/etc/mtree/BSD.tests.dist +++ b/etc/mtree/BSD.tests.dist @@ -871,6 +871,8 @@ .. virstor .. + zoned + .. .. .. kern @@ -1323,6 +1325,8 @@ .. traceroute .. + zonectl + .. .. .. diff --git a/include/Makefile b/include/Makefile index bfa82a0bb706..0cf926a6e00f 100644 --- a/include/Makefile +++ b/include/Makefile @@ -60,6 +60,7 @@ LSUBDIRS= dev/acpica dev/agp dev/ciss dev/filemon dev/firewire \ geom/cache geom/concat geom/eli geom/gate geom/journal geom/label \ geom/mirror geom/mountver geom/multipath geom/nop \ geom/raid geom/raid3 geom/shsec geom/stripe geom/union geom/virstor \ + geom/zoned \ net/altq \ net/route \ netgraph/netflow \ diff --git a/lib/geom/Makefile.classes b/lib/geom/Makefile.classes index 1852e35be15f..753495c9ff10 100644 --- a/lib/geom/Makefile.classes +++ b/lib/geom/Makefile.classes @@ -22,3 +22,4 @@ GEOM_CLASSES+= shsec GEOM_CLASSES+= stripe GEOM_CLASSES+= union GEOM_CLASSES+= virstor +GEOM_CLASSES+= zoned diff --git a/lib/geom/zoned/Makefile b/lib/geom/zoned/Makefile new file mode 100644 index 000000000000..d80b0081c477 --- /dev/null +++ b/lib/geom/zoned/Makefile @@ -0,0 +1,5 @@ +PACKAGE=geom + +GEOM_CLASS= zoned + +.include diff --git a/lib/geom/zoned/Makefile.depend b/lib/geom/zoned/Makefile.depend new file mode 100644 index 000000000000..0dd05cace3c0 --- /dev/null +++ b/lib/geom/zoned/Makefile.depend @@ -0,0 +1,16 @@ +# Autogenerated - do NOT edit! + +DIRDEPS = \ + include \ + include/xlocale \ + lib/${CSU_DIR} \ + lib/libc \ + lib/libcompiler_rt \ + lib/libgeom \ + + +.include + +.if ${DEP_RELDIR} == ${_DEP_RELDIR} +# local dependencies - needed for -jN in clean tree +.endif diff --git a/lib/geom/zoned/geom_zoned.c b/lib/geom/zoned/geom_zoned.c new file mode 100644 index 000000000000..2c112949fd7b --- /dev/null +++ b/lib/geom/zoned/geom_zoned.c @@ -0,0 +1,289 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2026 voidanix + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/geom.h" +#include "misc/subr.h" + +uint32_t lib_version = G_LIB_VERSION; +uint32_t version = G_ZONED_VERSION; + +#define GZONED_ZONESIZE "256M" + +static void zoned_main(struct gctl_req *req, unsigned flags); +static void zoned_create(struct gctl_req *req); +static void zoned_clear(struct gctl_req *req); + +struct g_command class_commands[] = { + { "clear", G_FLAG_VERBOSE, zoned_main, G_NULL_OPTS, "[-v] dev ..." }, + { "create", G_FLAG_VERBOSE | G_FLAG_LOADKLD, zoned_main, + { + { 's', "zonesize", GZONED_ZONESIZE, G_TYPE_NUMBER }, + { 'c', "conventional", "", G_TYPE_STRING }, + { 'm', "maxopen", "0", G_TYPE_NUMBER }, + { 'u', "restricted", NULL, G_TYPE_BOOL }, + G_OPT_SENTINEL + }, + "[-uv] [-s zonesize] [-c zone[-zone][,zone[-zone]]...] " + "[-m maxopen] dev" + }, + { "fault", G_FLAG_VERBOSE, NULL, + { + { 'z', "zone", NULL, G_TYPE_NUMBER }, + { 's', "state", NULL, G_TYPE_STRING }, + G_OPT_SENTINEL + }, + "[-v] -z zone -s ro|offline|reset|clear name" + }, + { "destroy", G_FLAG_VERBOSE, NULL, + { + { 'f', "force", NULL, G_TYPE_BOOL }, + G_OPT_SENTINEL + }, + "[-fv] name ..." }, + { "stop", G_FLAG_VERBOSE, NULL, + { + { 'f', "force", NULL, G_TYPE_BOOL }, + G_OPT_SENTINEL + }, + "[-fv] name ..." }, + G_CMD_SENTINEL +}; + +static int verbose = 0; + +static void +zoned_main(struct gctl_req *req, unsigned flags) +{ + const char *name; + + if ((flags & G_FLAG_VERBOSE) != 0) + verbose = 1; + + name = gctl_get_ascii(req, "verb"); + if (name == NULL) { + gctl_error(req, "No '%s' argument.", "verb"); + return; + } + if (strcmp(name, "create") == 0) + zoned_create(req); + else if (strcmp(name, "clear") == 0) + zoned_clear(req); + else + gctl_error(req, "Unknown command: %s.", name); +} + +/* + * Parse a conventional-zone specification into md_conv/md_nconv. + */ +static int +zoned_parse_conv(struct gctl_req *req, const char *spec, uint32_t nzones, + struct g_zoned_metadata *md) +{ + char *buf, *ep, *sp, *tok; + unsigned long first, last; + uint32_t i; + + md->md_nconv = 0; + if (*spec == '\0') + return (0); + buf = strdup(spec); + if (buf == NULL) { + gctl_error(req, "Cannot allocate memory."); + return (-1); + } + for (tok = strtok_r(buf, ",", &sp); tok != NULL; + tok = strtok_r(NULL, ",", &sp)) { + if (md->md_nconv == G_ZONED_MAXCONV) { + gctl_error(req, + "Too many conventional zone ranges (max %d).", + G_ZONED_MAXCONV); + goto fail; + } + errno = 0; + if (!isdigit((unsigned char)*tok)) + goto syntax; + first = strtoul(tok, &ep, 10); + if (ep == tok || errno != 0) + goto syntax; + if (*ep == '-') { + tok = ep + 1; + if (!isdigit((unsigned char)*tok)) + goto syntax; + last = strtoul(tok, &ep, 10); + if (ep == tok || *ep != '\0' || errno != 0) + goto syntax; + } else if (*ep == '\0') { + last = first; + } else { + goto syntax; + } + if (last < first) { + gctl_error(req, "Backwards zone range %lu-%lu.", + first, last); + goto fail; + } + if (last >= nzones) { + gctl_error(req, + "Zone %lu does not exist (zones 0-%u).", last, + nzones - 1); + goto fail; + } + for (i = 0; i < md->md_nconv; i++) { + uint32_t ofirst, olast; + + ofirst = md->md_conv[i].cr_first; + olast = ofirst + md->md_conv[i].cr_count - 1; + if (first <= olast && ofirst <= last) { + gctl_error(req, "Zone range %lu-%lu overlaps " + "%u-%u.", first, last, ofirst, olast); + goto fail; + } + } + md->md_conv[md->md_nconv].cr_first = first; + md->md_conv[md->md_nconv].cr_count = last - first + 1; + md->md_nconv++; + } + free(buf); + return (0); +syntax: + gctl_error(req, "Invalid conventional zone specification '%s'.", + spec); +fail: + free(buf); + return (-1); +} + +static void +zoned_create(struct gctl_req *req) +{ + struct g_zoned_metadata md; + u_char sector[512]; + const char *conv, *dev; + off_t msize, zonesize; + intmax_t maxopen; + unsigned int secsize; + uint32_t i, nconv, nzones; + int nargs; + + bzero(sector, sizeof(sector)); + bzero(&md, sizeof(md)); + nargs = gctl_get_int(req, "nargs"); + if (nargs != 1) { + gctl_error(req, + "Usage: create [-u] [-s zonesize] [-c zones] " + "[-m maxopen] dev"); + return; + } + zonesize = (off_t)gctl_get_intmax(req, "zonesize"); + conv = gctl_get_ascii(req, "conventional"); + dev = gctl_get_ascii(req, "arg0"); + + msize = g_get_mediasize(dev); + secsize = g_get_sectorsize(dev); + if (msize == 0 || secsize == 0) { + gctl_error(req, "Can't get information about %s: %s.", dev, + strerror(errno)); + return; + } + if (g_provider_is_host_managed(dev)) { + gctl_error(req, "Cannot create a zoned device on %s: the tail " + "of a host-managed zoned device cannot hold the metadata " + "and zone state table.", dev); + return; + } + if (zonesize <= 0 || (zonesize % secsize) != 0) { + gctl_error(req, "Zone size must be a positive multiple of %u.", + secsize); + return; + } + if ((uint64_t)zonesize / secsize > G_ZONED_MAXZONESECS) { + gctl_error(req, "Zone size %jd exceeds %u sectors.", + (intmax_t)zonesize, G_ZONED_MAXZONESECS); + return; + } + nzones = g_zoned_nzones(msize, zonesize, secsize); + if (nzones == 0) { + gctl_error(req, "Zone size %jd is unusable for %s: it must " + "yield at least one zone and at most %u.", + (intmax_t)zonesize, dev, G_ZONED_MAXZONES); + return; + } + if (zoned_parse_conv(req, conv, nzones, &md) != 0) + return; + maxopen = gctl_get_intmax(req, "maxopen"); + if (maxopen < 0 || maxopen > nzones) { + gctl_error(req, "Maximum open zones must be between 0 and " + "the number of zones (%u).", nzones); + return; + } + + memcpy(md.md_magic, G_ZONED_MAGIC, sizeof(G_ZONED_MAGIC)); + md.md_version = G_ZONED_VERSION; + md.md_id = arc4random(); + md.md_zonesize = zonesize; + md.md_sectorsize = secsize; + md.md_provsize = msize; + md.md_maxopen = (uint32_t)maxopen; + if (gctl_get_int(req, "restricted")) + md.md_flags |= G_ZONED_MD_RESTRICTED_READS; + + /* + * Hand the geometry to the kernel rather than writing it on the media + * and waiting for the taste. The metadata should be written only once + * the device is up: a request the kernel turns down leaves the provider + * untouched. + */ + zoned_metadata_encode(&md, sector); + gctl_ro_param(req, "metadata", sizeof(sector), sector); + if (gctl_issue(req) != NULL) + return; + if (verbose) { + nconv = 0; + for (i = 0; i < md.md_nconv; i++) + nconv += md.md_conv[i].cr_count; + printf("Device %s zoned: %u zones of %jd bytes" + " (%u conventional).\n", dev, nzones, (intmax_t)zonesize, + nconv); + } +} + +static void +zoned_clear(struct gctl_req *req) +{ + const char *name; + int error, i, nargs; + + nargs = gctl_get_int(req, "nargs"); + if (nargs < 1) { + gctl_error(req, "Missing device(s)."); + return; + } + + for (i = 0; i < nargs; i++) { + name = gctl_get_ascii(req, "arg%d", i); + error = g_metadata_clear(name, G_ZONED_MAGIC); + if (error != 0) { + fprintf(stderr, "Can't clear metadata on %s: %s.\n", + name, strerror(error)); + gctl_error(req, "Not fully done."); + continue; + } + if (verbose) + printf("Metadata cleared on %s.\n", name); + } +} diff --git a/lib/geom/zoned/gzoned.8 b/lib/geom/zoned/gzoned.8 new file mode 100644 index 000000000000..854eeed74854 --- /dev/null +++ b/lib/geom/zoned/gzoned.8 @@ -0,0 +1,260 @@ +.\" SPDX-License-Identifier: BSD-2-Clause +.\" +.\" Copyright (c) 2026 voidanix +.\" +.Dd August 4, 2026 +.Dt GZONED 8 +.Os +.Sh NAME +.Nm gzoned +.Nd "emulate a host-managed zoned device on top of a regular provider" +.Sh SYNOPSIS +.Nm +.Cm create +.Op Fl uv +.Op Fl s Ar zonesize +.Op Fl c Ar zones +.Op Fl m Ar maxopen +.Ar dev +.Nm +.Cm fault +.Op Fl v +.Fl z Ar zone +.Fl s Ar state +.Ar name +.Nm +.Cm destroy +.Op Fl fv +.Ar name ... +.Nm +.Cm stop +.Op Fl fv +.Ar name ... +.Nm +.Cm clear +.Op Fl v +.Ar dev ... +.Nm +.Cm list +.Nm +.Cm status +.Nm +.Cm load +.Nm +.Cm unload +.Sh DESCRIPTION +The +.Nm +utility presents to the system a host-managed zoned block device backed by a +regular, non-zoned provider. +The exposed device is of the kind described by the +SCSI Zoned Block Commands (ZBC) and ATA Zoned ATA Command Set (ZAC) +specifications. +.Pp +The backing provider is divided into equally sized, sequential-write-required +zones, unless marked conventional by the +.Fl c +option. +For each zone the GEOM class maintains a write pointer and answers the +zone-management commands +.Pq see Xr zonectl 8 +issued through the +.Dv DIOCZONECMD +ioctl. +.Pp +Resetting a write pointer issues a best-effort +.Dv BIO_DELETE +for the zone's range to the backing provider, resembling a real drives +de-allocating a reset zone. +.Dv BIO_DELETE +requests to the zoned device itself are rejected, as zoned devices have no +unmap. +Sequential zones are reclaimed by resetting the write pointer. +.Pp +The configuration for a device is persistent. +A metadata block and the per-zone state are stored in a small area reserved at +the tail of the backing provider, so the zoned device is recreated +automatically whenever the provider appears +.Pq at boot, module load or hot-plug , +with its write pointers restored. +Note that because of the metadata location on the backing provider, the zoned +device is torn down if the backing provider is resized with unflushed zone state +changes being discarded. +Zone state changes are committed to the backing provider on a BIO_FLUSH. +Changes made since the last flush may be rolled back by an unclean shutdown, +resembling a drive whose zone state is volatile until flushed. +.Pp +The first argument to +.Nm +indicates an action to be performed: +.Bl -tag -width ".Cm destroy" +.It Cm create +Mark +.Ar dev +as zoned, creating a device named after the backing provider with +.Dq .zoned +appended +.Pq e.g. Pa /dev/da0.zoned . +The number of zones is the usable size of +.Ar dev +divided by +.Ar zonesize ; +a small reservation at the tail of +.Ar dev +holds the zone metadata; any trailing space that does not make up a full zone +is not exposed. +Host-managed zoned providers are refused: their tail normally lies in a +sequential-write-required zone and cannot hold the metadata and zone state +table. +.It Cm fault +Inject a zone fault into the running device +.Ar name . +.Fl z Ar zone +selects the zone by number and +.Fl s Ar state +is one of +.Ar ro +.Pq Ar readonly , +.Ar offline , +.Ar reset +.Pq zone needs a write-pointer reset +or +.Ar clear +.Pq return the zone to its normal state implied by its write pointer . +Readonly and offline zones reject writes and zone-management commands; +the fault persists with the rest of the zone state until cleared. +.It Cm clear +Permanently un-zone the given backing device(s) by clearing the on-disk +metadata. +A backing device carrying a live zoned device is held open and cannot be +cleared: tear the device down with +.Cm stop +first. +.It Cm stop +Tear down the given zoned device(s) without touching the on-disk metadata. +.Ar name +is the zoned device +.Pq e.g. Pa da0.zoned . +Unflushed zone state is committed to the backing provider first. +The device comes back the next time the backing provider is tasted +.Pq e.g. at boot or when it is opened and closed again . +.It Cm destroy +Same as +.Cm stop . +.It Cm list +See +.Xr geom 8 . +.It Cm status +See +.Xr geom 8 . +.It Cm load +See +.Xr geom 8 . +.It Cm unload +See +.Xr geom 8 . +.El +.Pp +Additional options: +.Bl -tag -width ".Fl s Ar zonesize" +.It Fl c Ar zones +Mark the given zones as conventional +.Pq randomly writable, no write pointer . +.Ar zones +is a comma-separated list of zone numbers and inclusive ranges, counted +from zero, e.g.\& +.Ar 60 , +.Ar 60-70 +or +.Ar 0-3,60,90-95 . +At most 16 comma-separated entries may be given. +By default all zones are sequential-write-required. +.It Fl f +Tear down the device even if it is still open. +.It Fl m Ar maxopen +Limit the number of simultaneously open sequential zones. +Once the limit is reached, implicitly opened zones are implicitly closed to make +room. +If every open zone was opened explicitly, further writes and explicit opens fail +with +.Er ENOSPC . +The default of 0 means unlimited. +.It Fl s Ar zonesize +Size of a single zone. +The value must be a multiple of the backing provider's sector size and may use +the usual size suffixes +.Pq e.g. Ar 7G , 64M . +The default is +.Ar 256M . +It must also leave the device with at least one, and at most 4294967295 zones. +.It Fl u +Restrict reads in sequential-write-required zones +.Pq unset URSWRZ bit . +Similarly to real zoned drives, the device supports unrestricted reads that are +passed through unchecked by default. +With +.Fl u +the device instead reports URSWRZ as unset and rejects with +.Er EIO +any read in a sequential zone that extends beyond that zone's write pointer, as +well as any read that crosses out of a conventional zone into a sequential one. +Note that this renders the device unreadable to zone-unaware consumers. +Tasting the zoned provider +.Pq e.g. partition table scanning +will also fail until the accessed zones have been written. +.It Fl v +Be more verbose. +.El +.Sh SYSCTL VARIABLES +The following +.Xr sysctl 8 +variable can be used to control the behavior of the +.Nm ZONED +GEOM class. +.Bl -tag -width indent +.It Va kern.geom.zoned.debug : No 0 +Debug level of the +.Nm ZONED +GEOM class. +This can be set to a number between 0 and 3 inclusive. +If set to 0, minimal debug information is printed; if set to 3, the maximum +amount of debug information is printed. +.El +.Sh EXIT STATUS +.Ex -std +.Sh EXAMPLES +Expose the memory disk +.Pa /dev/md0 +as a host-managed zoned device +.Pa /dev/md0.zoned +with one-gigabyte zones, the first four of which are conventional. +Inspect its zones, then permanently un-zone it: +.Bd -literal -offset indent +mdconfig -t malloc -s 10G -u md0 +gzoned create -s 1G -c 0-3 /dev/md0 +zonectl -d /dev/md0.zoned -c params +zonectl -d /dev/md0.zoned -c rz +gzoned clear /dev/md0 +.Ed +.Pp +The same operations are also available through the +.Xr geom 8 +front end: +.Bd -literal -offset indent +geom zoned create -s 128M /dev/da0 +geom zoned stop da0.zoned +geom zoned clear /dev/da0 +.Ed +.Sh SEE ALSO +.Xr geom 4 , +.Xr md 4 , +.Xr geom 8 , +.Xr mdconfig 8 , +.Xr zonectl 8 +.Sh HISTORY +The +.Nm +utility appeared in +.Fx 16.0 . +.Sh AUTHORS +.An voidanix Aq Mt voidanix@FreeBSD.org diff --git a/sbin/geom/core/geom.8 b/sbin/geom/core/geom.8 index 96db7f3a80e4..ad50334d324f 100644 --- a/sbin/geom/core/geom.8 +++ b/sbin/geom/core/geom.8 @@ -145,12 +145,12 @@ are: .Pp .Bl -column "* MULTIPATH" "* MULTIPATH" "* MULTIPATH" -offset indent .It \(bu CACHE Ta \(bu MIRROR Ta \(bu SCHED -.It \(bu CONCAT Ta \(bu MOUNTVER Ta \(bu SHSEC +.It \(bu CONCAT Ta \(bu MOUNTVER Ta \(bu SHSEC .It \(bu DISK Ta \(bu MULTIPATH Ta \(bu STRIPE .It \(bu ELI Ta \(bu NOP Ta \(bu UNION .It \(bu JOURNAL Ta \(bu PART Ta \(bu VFS .It \(bu LABEL Ta \(bu RAID Ta \(bu VIRSTOR -.It \(bu MD Ta \(bu RAID3 +.It \(bu MD Ta \(bu RAID3 Ta \(bu ZONED .El .Sh ENVIRONMENT The following environment variables affect the execution of @@ -202,7 +202,8 @@ geom md unload .Xr gshsec 8 , .Xr gstripe 8 , .Xr gunion 8 , -.Xr gvirstor 8 +.Xr gvirstor 8 , +.Xr gzoned 8 .Sh HISTORY The .Nm diff --git a/sbin/geom/misc/subr.c b/sbin/geom/misc/subr.c index 18bac7108ed0..8e5e78258268 100644 --- a/sbin/geom/misc/subr.c +++ b/sbin/geom/misc/subr.c @@ -28,7 +28,9 @@ #include #include +#include #include +#include #include #include #include @@ -218,6 +220,24 @@ g_get_sectorsize(const char *name) return ((unsigned int)sectorsize); } +int +g_provider_is_host_managed(const char *name) +{ + struct disk_zone_args zone_args; + int error, fd; + + bzero(&zone_args, sizeof(zone_args)); + zone_args.zone_cmd = DISK_ZONE_GET_PARAMS; + fd = g_open(name, 0); + if (fd == -1) + return (0); + error = ioctl(fd, DIOCZONECMD, &zone_args); + (void)g_close(fd); + return (error == 0 && + zone_args.zone_params.disk_params.zone_mode == + DISK_ZONE_MODE_HOST_MANAGED); +} + int g_metadata_read(const char *name, unsigned char *md, size_t size, const char *magic) diff --git a/sbin/geom/misc/subr.h b/sbin/geom/misc/subr.h index 708abf5f8767..0090543d1e39 100644 --- a/sbin/geom/misc/subr.h +++ b/sbin/geom/misc/subr.h @@ -36,6 +36,7 @@ int g_parse_lba(const char *lbastr, unsigned int sectorsize, off_t *sectors); off_t g_get_mediasize(const char *name); unsigned int g_get_sectorsize(const char *name); +int g_provider_is_host_managed(const char *name); int g_metadata_read(const char *name, unsigned char *md, size_t size, const char *magic); diff --git a/sys/geom/zoned/g_zoned.c b/sys/geom/zoned/g_zoned.c new file mode 100644 index 000000000000..5f9773273551 --- /dev/null +++ b/sys/geom/zoned/g_zoned.c @@ -0,0 +1,1819 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2026 voidanix + */ + +/* + * GEOM_ZONED presents a plain (non-zoned) provider to the rest of the system + * as a host-managed zoned block device. The medium is divided into equal + * sequential-write-required zones. This GEOM class tracks a write pointer per + * zone and answers BIO_ZONE management commands similarly to a real ZBC/ZAC + * drive would. + * + * Persistence: + * - The provider's last sector holds a metadata block, written by the + * userland "create" command. The kernel tastes it on every provider + * arrival and re-creates the zoned device automatically. + * - The sectors just before it hold the per-zone state (condition + write + * pointer). That table is read at taste time and rewritten lazily. + * Zone-state changes, zone-management commands included, only mark the + * table dirty; BIO_FLUSH is what commits them, mirroring a drive whose + * zone state is volatile until a cache flush. Changes since the last + * flush may be rolled back by an unclean shutdown. + */ + +#define EXTERR_CATEGORY EXTERR_CAT_GEOMZONED + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +FEATURE(geom_zoned, "GEOM Zoned Storage Medium emulation"); + +static MALLOC_DEFINE(M_ZONED, "zoned_data", "GEOM_ZONED Data"); + +SYSCTL_DECL(_kern_geom); +static SYSCTL_NODE(_kern_geom, OID_AUTO, zoned, CTLFLAG_RW | CTLFLAG_MPSAFE, 0, + "GEOM_ZONED stuff"); +static u_int g_zoned_debug = 0; +SYSCTL_UINT(_kern_geom_zoned, OID_AUTO, debug, CTLFLAG_RW, &g_zoned_debug, 0, + "Debug level"); + +#define G_ZONED_EXTERR(bp, error, ...) ({ \ + (bp)->bio_flags |= BIO_EXTERR; \ + EXTERROR_KE(&(bp)->bio_exterr, (error), __VA_ARGS__); \ +}) + +static g_access_t g_zoned_access; +static g_ctl_req_t g_zoned_config; +static g_ctl_destroy_geom_t g_zoned_destroy_geom; +static g_dumpconf_t g_zoned_dumpconf; +static g_orphan_t g_zoned_orphan; +static g_provgone_t g_zoned_providergone; +static g_resize_t g_zoned_resize; +static g_start_t g_zoned_start; +static g_taste_t g_zoned_taste; + +static struct g_class g_zoned_class = { + .name = G_ZONED_CLASS_NAME, + .version = G_VERSION, + .ctlreq = g_zoned_config, + .destroy_geom = g_zoned_destroy_geom, + .taste = g_zoned_taste, + .access = g_zoned_access, + .dumpconf = g_zoned_dumpconf, + .orphan = g_zoned_orphan, + .providergone = g_zoned_providergone, + .resize = g_zoned_resize, + .spoiled = g_zoned_orphan, + .start = g_zoned_start, +}; + +/* + * Index of the zone that contains the given LBA. Caller must have range-checked + * the LBA against sc_maxlba. + */ +static __inline uint32_t +g_zoned_zoneno(struct g_zoned_softc *sc, uint64_t lba) +{ + + return ((uint32_t)(lba / sc->sc_zonesecs)); +} + +static bool +g_zoned_is_conv(const struct g_zoned_softc *sc, uint32_t zno) +{ + const struct g_zoned_convrange *cr; + uint32_t i; + + for (i = 0; i < sc->sc_nconv; i++) { + cr = &sc->sc_conv[i]; + if (zno >= cr->cr_first && zno - cr->cr_first < cr->cr_count) + return (true); + } + return (false); +} + +/* + * Extend the dirty zone range to cover zone zno, so the next BIO_FLUSH + * writes its table entry out. Must be called with sc_lock held. + */ +static void +g_zoned_mark_dirty(struct g_zoned_softc *sc, uint32_t zno) +{ + + mtx_assert(&sc->sc_lock, MA_OWNED); + + if (!sc->sc_dirty) { + sc->sc_dirty = true; + sc->sc_dirtylo = sc->sc_dirtyhi = zno; + } else { + if (zno < sc->sc_dirtylo) + sc->sc_dirtylo = zno; + if (zno > sc->sc_dirtyhi) + sc->sc_dirtyhi = zno; + } +} + +static bool +g_zoned_cond_is_open(uint8_t cond) +{ + + return (cond == DISK_ZONE_COND_IMPLICIT_OPEN || + cond == DISK_ZONE_COND_EXPLICIT_OPEN); +} + +/* + * Put a zone into a new condition, maintaining the count of open + * zones. Must be called with sc_lock held. + */ +static void +g_zoned_set_cond(struct g_zoned_softc *sc, struct disk_zone_rep_entry *z, + uint8_t cond) +{ + bool is_open, want_open; + + mtx_assert(&sc->sc_lock, MA_OWNED); + + is_open = g_zoned_cond_is_open(z->zone_condition); + want_open = g_zoned_cond_is_open(cond); + if (!is_open && want_open) + sc->sc_nopen++; + else if (is_open && !want_open) + sc->sc_nopen--; + z->zone_condition = cond; +} + +/* + * Make room to open one more zone under the open-zones limit by implicitly + * closing the lowest-numbered implicitly-open zone, similarly to a real drive. + */ +static bool +g_zoned_open_room(struct g_zoned_softc *sc) +{ + struct disk_zone_rep_entry *z; + uint32_t i; + + mtx_assert(&sc->sc_lock, MA_OWNED); + + if (sc->sc_maxopen == 0 || sc->sc_nopen < sc->sc_maxopen) + return (true); + for (i = 0; i < sc->sc_nzones; i++) { + z = &sc->sc_zones[i]; + if (z->zone_condition == DISK_ZONE_COND_IMPLICIT_OPEN) { + g_zoned_set_cond(sc, z, DISK_ZONE_COND_CLOSED); + g_zoned_mark_dirty(sc, i); + return (true); + } + } + return (false); +} + +/* + * Put every zone into its pure state, with the type coming from the + * metadata-defined conventional ranges. + */ +static void +g_zoned_reset_zones(struct g_zoned_softc *sc) +{ + struct disk_zone_rep_entry *z; + uint32_t i; + + sc->sc_nopen = 0; + for (i = 0; i < sc->sc_nzones; i++) { + z = &sc->sc_zones[i]; + z->zone_start_lba = (uint64_t)i * sc->sc_zonesecs; + z->zone_length = sc->sc_zonesecs; + z->zone_flags = 0; + if (g_zoned_is_conv(sc, i)) { + z->zone_type = DISK_ZONE_TYPE_CONVENTIONAL; + z->zone_condition = DISK_ZONE_COND_NOT_WP; + /* Conventional zones have no write pointer. */ + z->write_pointer_lba = G_ZONED_WP_NONE_LBA; + } else { + z->zone_type = DISK_ZONE_TYPE_SEQ_REQUIRED; + z->zone_condition = DISK_ZONE_COND_EMPTY; + z->write_pointer_lba = z->zone_start_lba; + } + } +} + +/* + * Mark the whole table (header included) dirty so that the first BIO_FLUSH + * writes it out. + */ +static void +g_zoned_dirty_table(struct g_zoned_softc *sc) +{ + + sc->sc_dirty = true; + sc->sc_hdrdirty = true; + sc->sc_dirtylo = 0; + sc->sc_dirtyhi = sc->sc_nzones - 1; +} + +/* + * Read the existing persistent zone table from the provider into the live zone + * array, otherwise initialise an empty one if no valid table is present. + */ +static void +g_zoned_load_table(struct g_zoned_softc *sc, struct g_consumer *cp) +{ + struct g_zoned_table_hdr th; + struct disk_zone_rep_entry *z; + u_char *buf; + off_t off, resid, chunk; + uint32_t i, n, zno; + int error; + bool valid; + + g_topology_assert(); + g_topology_unlock(); + + valid = false; + buf = g_read_data(cp, sc->sc_taboff, sc->sc_secsize, &error); + if (buf != NULL) { + zoned_table_hdr_decode(buf, &th); + valid = memcmp(th.th_magic, G_ZONED_TABLE_MAGIC, + sizeof(G_ZONED_TABLE_MAGIC)) == 0 && + th.th_version == G_ZONED_VERSION && + th.th_nzones == sc->sc_nzones; + g_free(buf); + } + + zno = 0; + off = sc->sc_taboff + sc->sc_secsize; + resid = (off_t)(sc->sc_tabsecs - 1) * sc->sc_secsize; + while (valid && resid > 0) { + chunk = MIN(resid, (off_t)maxphys); + chunk -= chunk % sc->sc_secsize; + if (chunk == 0) + chunk = sc->sc_secsize; + buf = g_read_data(cp, off, chunk, &error); + if (buf == NULL) { + valid = false; + break; + } + n = MIN((uint32_t)(chunk / G_ZONED_ENTRY_SIZE), + sc->sc_nzones - zno); + for (i = 0; i < n; i++) + g_zoned_entry_decode(buf + i * G_ZONED_ENTRY_SIZE, + &sc->sc_zones[zno + i], + (uint64_t)(zno + i) * sc->sc_zonesecs); + g_free(buf); + zno += n; + off += chunk; + resid -= chunk; + } + + g_topology_lock(); + + if (!valid) { + G_ZONED_DEBUG(1, "No valid zone table on %s; initialising.", + cp->provider->name); + g_zoned_reset_zones(sc); + g_zoned_dirty_table(sc); + return; + } + + for (i = 0; i < sc->sc_nzones; i++) { + z = &sc->sc_zones[i]; + z->zone_start_lba = (uint64_t)i * sc->sc_zonesecs; + z->zone_length = sc->sc_zonesecs; + /* The metadata dictates zone types, not the table. */ + if (g_zoned_is_conv(sc, i)) { + z->zone_type = DISK_ZONE_TYPE_CONVENTIONAL; + z->zone_condition = DISK_ZONE_COND_NOT_WP; + z->write_pointer_lba = G_ZONED_WP_NONE_LBA; + continue; + } + z->zone_type = DISK_ZONE_TYPE_SEQ_REQUIRED; + /* Guard against a corrupt write pointer. */ + if (z->write_pointer_lba < z->zone_start_lba || + z->write_pointer_lba > + z->zone_start_lba + z->zone_length) { + z->write_pointer_lba = z->zone_start_lba; + z->zone_condition = DISK_ZONE_COND_EMPTY; + } + } + sc->sc_nopen = 0; + for (i = 0; i < sc->sc_nzones; i++) + if (g_zoned_cond_is_open(sc->sc_zones[i].zone_condition)) + sc->sc_nopen++; + G_ZONED_DEBUG(1, "Restored zone table from %s.", cp->provider->name); +} + +/* + * "Flush the dirty table sectors, then forward the cache flush" thing. We + * allocate one of those per BIO_FLUSH that finds dirty state to commit. + */ +struct g_zoned_flush { + struct bio *fl_orig; /* Original BIO_FLUSH. */ + struct g_consumer *fl_cp; /* Where to send the I/O. */ + struct g_zoned_softc *fl_sc; + u_char *fl_buf; /* Snapshot of dirty sectors. */ + off_t fl_off; /* Disk offset of first sector. */ + off_t fl_total; /* Bytes to write. */ + off_t fl_done; /* Bytes written so far. */ + u_int fl_secsize; + /* Dirty state the snapshot took: to put back if the write fails. */ + uint32_t fl_dirtylo; + uint32_t fl_dirtyhi; + bool fl_hdrdirty; +}; + +static void g_zoned_flush_step(struct g_zoned_flush *fc); + +static void +g_zoned_flush_final(struct bio *bp) +{ + struct g_zoned_flush *fc = bp->bio_caller1; + struct bio *orig = fc->fl_orig; + int error = bp->bio_error; + + g_destroy_bio(bp); + g_free(fc->fl_buf); + g_free(fc); + g_io_deliver(orig, error); +} + +static void +g_zoned_flush_write_done(struct bio *bp) +{ + struct g_zoned_flush *fc = bp->bio_caller1; + int error = bp->bio_error; + + g_destroy_bio(bp); + if (error != 0) { + struct g_zoned_softc *sc = fc->fl_sc; + + G_ZONED_DEBUG(0, "Zone table write failed (error=%d).", error); + /* + * Nothing reached the medium, thus the state the snapshot + * cleared is still only in memory. Mark it dirty again, + * widening whatever has been dirtied since, so that the next + * flush tries once more instead of leaving the table stale. + */ + mtx_lock(&sc->sc_lock); + g_zoned_mark_dirty(sc, fc->fl_dirtylo); + g_zoned_mark_dirty(sc, fc->fl_dirtyhi); + sc->sc_hdrdirty |= fc->fl_hdrdirty; + mtx_unlock(&sc->sc_lock); + g_free(fc->fl_buf); + g_io_deliver(fc->fl_orig, error); + g_free(fc); + return; + } + g_zoned_flush_step(fc); +} + +static void +g_zoned_flush_step(struct g_zoned_flush *fc) +{ + struct bio *cbp; + off_t chunk; + + if (fc->fl_done < fc->fl_total) { + chunk = fc->fl_total - fc->fl_done; + if (chunk > (off_t)maxphys) { + chunk = maxphys; + chunk -= chunk % fc->fl_secsize; + } + cbp = g_alloc_bio(); + cbp->bio_cmd = BIO_WRITE; + cbp->bio_offset = fc->fl_off + fc->fl_done; + cbp->bio_data = fc->fl_buf + fc->fl_done; + cbp->bio_length = chunk; + cbp->bio_done = g_zoned_flush_write_done; + cbp->bio_caller1 = fc; + fc->fl_done += chunk; + g_io_request(cbp, fc->fl_cp); + return; + } + /* Table is on its way down; now forward the real cache flush. */ + cbp = g_alloc_bio(); + cbp->bio_cmd = BIO_FLUSH; + cbp->bio_done = g_zoned_flush_final; + cbp->bio_caller1 = fc; + g_io_request(cbp, fc->fl_cp); +} + +/* + * Snapshot the dirty part of the table into a freshly allocated buffer of + * whole sectors, encoded in the on-disk format and clear the dirty state. + */ +static u_char * +g_zoned_encode_dirty(struct g_zoned_softc *sc, off_t *offp, off_t *totalp) +{ + struct g_zoned_table_hdr th; + u_char *buf; + off_t total; + uint32_t first, i, limit, seclo, sechi, zpersec; + + mtx_assert(&sc->sc_lock, MA_OWNED); + + if (!sc->sc_dirty) + return (NULL); + /* Table sector 0 holds the header; entries start at sector 1. */ + zpersec = sc->sc_secsize / G_ZONED_ENTRY_SIZE; + seclo = sc->sc_hdrdirty ? 0 : 1 + sc->sc_dirtylo / zpersec; + sechi = 1 + sc->sc_dirtyhi / zpersec; + total = (off_t)(sechi - seclo + 1) * sc->sc_secsize; + buf = g_malloc(total, M_NOWAIT | M_ZERO); + if (buf == NULL) + return (NULL); + if (seclo == 0) { + memset(&th, 0, sizeof(th)); + memcpy(th.th_magic, G_ZONED_TABLE_MAGIC, + sizeof(G_ZONED_TABLE_MAGIC)); + th.th_version = G_ZONED_VERSION; + th.th_nzones = sc->sc_nzones; + zoned_table_hdr_encode(&th, buf); + } + /* Encode every entry falling into the sectors being written. */ + first = (seclo == 0) ? 0 : (seclo - 1) * zpersec; + limit = MIN(sc->sc_nzones, sechi * zpersec); + for (i = first; i < limit; i++) + g_zoned_entry_encode(&sc->sc_zones[i], buf + + (seclo == 0 ? sc->sc_secsize : 0) + + (off_t)(i - first) * G_ZONED_ENTRY_SIZE); + sc->sc_dirty = false; + sc->sc_hdrdirty = false; + *offp = sc->sc_taboff + (off_t)seclo * sc->sc_secsize; + *totalp = total; + return (buf); +} + +/* + * Attempt commiting the dirty zone-table state for BIO_FLUSH. Returns true + * if it took ownership of bp i.e. an async chain is running or the flush has + * failed, false if the caller should forward the flush normally. + */ +static bool +g_zoned_flush_begin(struct g_zoned_softc *sc, struct g_geom *gp, + struct bio *bp) +{ + struct g_zoned_flush *fc; + struct g_consumer *cp; + u_char *buf; + off_t off, total; + uint32_t dirtylo, dirtyhi; + bool dirty, hdrdirty; + + cp = LIST_FIRST(&gp->consumer); + /* Persistence is not possible without write access; just forward. */ + if (cp->acw == 0) + return (false); + + fc = g_malloc(sizeof(*fc), M_NOWAIT); + + mtx_lock(&sc->sc_lock); + dirty = sc->sc_dirty; + dirtylo = sc->sc_dirtylo; + dirtyhi = sc->sc_dirtyhi; + hdrdirty = sc->sc_hdrdirty; + /* Leave the state dirty for the next flush if nothing can carry it. */ + buf = (fc != NULL) ? g_zoned_encode_dirty(sc, &off, &total) : NULL; + mtx_unlock(&sc->sc_lock); + if (buf == NULL) { + g_free(fc); + /* Nothing to commit, so the flush stands on its own. */ + if (!dirty) + return (false); + /* + * The table stays in memory, thus this flush cannot claim it + * reached the medium. Say so, rather than forwarding a flush + * that commits none of the state the caller is flushing for. + */ + G_ZONED_DEBUG(0, "No memory to commit the zone table;" + " failing the flush."); + g_io_deliver(bp, G_ZONED_EXTERR(bp, ENOMEM, + "No memory to commit the zone table.")); + return (true); + } + + fc->fl_dirtylo = dirtylo; + fc->fl_dirtyhi = dirtyhi; + fc->fl_hdrdirty = hdrdirty; + fc->fl_orig = bp; + fc->fl_cp = cp; + fc->fl_sc = sc; + fc->fl_buf = buf; + fc->fl_off = off; + fc->fl_total = total; + fc->fl_done = 0; + fc->fl_secsize = sc->sc_secsize; + g_zoned_flush_step(fc); + return (true); +} + +/* + * Best-effort, synchronous commit of the dirty zone-table state, for the + * destroy path where no further BIO_FLUSH will arrive. A failure means the + * on-disk table stays as of the last flush. + */ +static void +g_zoned_flush_sync(struct g_zoned_softc *sc, struct g_consumer *cp) +{ + u_char *buf; + off_t chunk, done, off, total; + int error; + + g_topology_assert(); + + /* A provider that went away takes the table's location with it. */ + if (cp == NULL || cp->provider == NULL || cp->provider->error != 0) + return; + mtx_lock(&sc->sc_lock); + buf = g_zoned_encode_dirty(sc, &off, &total); + mtx_unlock(&sc->sc_lock); + if (buf == NULL) + return; + g_topology_unlock(); + error = 0; + for (done = 0; done < total; done += chunk) { + chunk = MIN(total - done, (off_t)maxphys); + chunk -= chunk % sc->sc_secsize; + if (chunk == 0) + chunk = sc->sc_secsize; + error = g_write_data(cp, off + done, buf + done, chunk); + if (error != 0) + break; + } + if (error == 0) + error = g_io_flush(cp); + if (error != 0) + G_ZONED_DEBUG(0, + "Final zone table write on %s failed (error=%d).", + cp->provider->name, error); + g_free(buf); + g_topology_lock(); +} + +/* + * Test a zone against a REPORT ZONES reporting option (DISK_ZONE_REP_*). + */ +static bool +g_zoned_rep_match(const struct disk_zone_rep_entry *z, uint8_t rep_option) +{ + + switch (rep_option) { + case DISK_ZONE_REP_ALL: + return (true); + case DISK_ZONE_REP_EMPTY: + return (z->zone_condition == DISK_ZONE_COND_EMPTY); + case DISK_ZONE_REP_IMP_OPEN: + return (z->zone_condition == DISK_ZONE_COND_IMPLICIT_OPEN); + case DISK_ZONE_REP_EXP_OPEN: + return (z->zone_condition == DISK_ZONE_COND_EXPLICIT_OPEN); + case DISK_ZONE_REP_CLOSED: + return (z->zone_condition == DISK_ZONE_COND_CLOSED); + case DISK_ZONE_REP_FULL: + return (z->zone_condition == DISK_ZONE_COND_FULL); + case DISK_ZONE_REP_READONLY: + return (z->zone_condition == DISK_ZONE_COND_READONLY); + case DISK_ZONE_REP_OFFLINE: + return (z->zone_condition == DISK_ZONE_COND_OFFLINE); + case DISK_ZONE_REP_RWP: + return ((z->zone_flags & DISK_ZONE_FLAG_RESET) != 0); + case DISK_ZONE_REP_NON_SEQ: + return ((z->zone_flags & DISK_ZONE_FLAG_NON_SEQ) != 0); + case DISK_ZONE_REP_NON_WP: + return (z->zone_condition == DISK_ZONE_COND_NOT_WP); + default: + return (false); + } +} + +static bool +g_zoned_rep_option_valid(uint8_t rep_option) +{ + + return (rep_option <= DISK_ZONE_REP_OFFLINE || + rep_option == DISK_ZONE_REP_RWP || + rep_option == DISK_ZONE_REP_NON_SEQ || + rep_option == DISK_ZONE_REP_NON_WP); +} + +/* + * Best-effort BIO_DELETE of just-reset zones, so that backing stores can + * reclaim the space, similarly to real drives de-allocating a reset zone. + */ +struct g_zoned_punch { + struct bio *pu_orig; /* Original BIO_ZONE. */ + u_int pu_pending; /* Outstanding deletes + 1. */ +}; + +static void +g_zoned_punch_rele(struct g_zoned_punch *pc) +{ + + if (atomic_fetchadd_int(&pc->pu_pending, -1) == 1) { + g_io_deliver(pc->pu_orig, 0); + g_free(pc); + } +} + +static void +g_zoned_punch_done(struct bio *bp) +{ + struct g_zoned_punch *pc = bp->bio_caller1; + + g_destroy_bio(bp); + g_zoned_punch_rele(pc); +} + +static void +g_zoned_punch_queue(struct g_zoned_softc *sc, struct bio_queue_head *queue, + const struct disk_zone_rep_entry *z) +{ + struct bio *dbp; + + mtx_assert(&sc->sc_lock, MA_OWNED); + + dbp = g_new_bio(); + if (dbp == NULL) { + G_ZONED_DEBUG(1, "No memory to reclaim zone %ju.", + (uint64_t)g_zoned_zoneno(sc, z->zone_start_lba)); + return; + } + dbp->bio_cmd = BIO_DELETE; + dbp->bio_offset = (off_t)z->zone_start_lba * sc->sc_secsize; + dbp->bio_length = sc->sc_zonesize; + dbp->bio_data = NULL; + bioq_insert_tail(queue, dbp); +} + +static void +g_zoned_punch_drain(struct bio_queue_head *queue) +{ + struct bio *dbp; + + while ((dbp = bioq_takefirst(queue)) != NULL) + g_destroy_bio(dbp); +} + +static void +g_zoned_punch_issue(struct bio *bp, struct g_consumer *cp, + struct bio_queue_head *queue) +{ + struct g_zoned_punch *pc; + struct bio *dbp; + + pc = (bioq_first(queue) != NULL) ? g_malloc(sizeof(*pc), M_NOWAIT) : + NULL; + if (pc == NULL) { + g_zoned_punch_drain(queue); + g_io_deliver(bp, 0); + return; + } + pc->pu_orig = bp; + pc->pu_pending = 1; + while ((dbp = bioq_takefirst(queue)) != NULL) { + dbp->bio_done = g_zoned_punch_done; + dbp->bio_caller1 = pc; + atomic_add_int(&pc->pu_pending, 1); + g_io_request(dbp, cp); + } + g_zoned_punch_rele(pc); +} + +/* + * Emulate BIO_ZONE management commands. + */ +static void +g_zoned_zonecmd(struct bio *bp, struct g_zoned_softc *sc) +{ + struct disk_zone_args *args = &bp->bio_zone; + uint32_t i, first, limit; + + switch (args->zone_cmd) { + case DISK_ZONE_GET_PARAMS: { + struct disk_zone_disk_params *p = + &args->zone_params.disk_params; + + p->zone_mode = DISK_ZONE_MODE_HOST_MANAGED; + p->flags = DISK_ZONE_RZ_SUP | DISK_ZONE_OPEN_SUP | + DISK_ZONE_CLOSE_SUP | DISK_ZONE_FINISH_SUP | + DISK_ZONE_RWP_SUP | DISK_ZONE_MAX_SEQ_SET; + if (!sc->sc_rdrestrict) + p->flags |= DISK_ZONE_DISK_URSWRZ; + /* Optimal zone counts are host-aware concepts; leave unset. */ + p->optimal_seq_zones = 0; + p->optimal_nonseq_zones = 0; + p->max_seq_zones = (sc->sc_maxopen != 0) ? sc->sc_maxopen : + G_ZONED_SEQ_UNLIMITED; + g_io_deliver(bp, 0); + return; + } + case DISK_ZONE_REPORT_ZONES: { + struct disk_zone_report *rep = &args->zone_params.report; + uint32_t filled, zno; + + if (!g_zoned_rep_option_valid(rep->rep_options)) { + g_io_deliver(bp, G_ZONED_EXTERR(bp, EINVAL, + "Unsupported reporting option. opt=%ju", + (uint64_t)rep->rep_options)); + return; + } + + mtx_lock(&sc->sc_lock); + sc->sc_zonecmds++; + rep->header.same = (sc->sc_convzones == 0 || + sc->sc_convzones == sc->sc_nzones) ? + DISK_ZONE_SAME_ALL_SAME : DISK_ZONE_SAME_TYPES_DIFFERENT; + rep->header.maximum_lba = sc->sc_maxlba - 1; + + if (rep->starting_id >= sc->sc_maxlba) + zno = sc->sc_nzones; + else + zno = g_zoned_zoneno(sc, rep->starting_id); + + rep->entries_available = 0; + filled = 0; + for (; zno < sc->sc_nzones; zno++) { + if (!g_zoned_rep_match(&sc->sc_zones[zno], + rep->rep_options)) + continue; + rep->entries_available++; + if (filled < rep->entries_allocated && + rep->entries != NULL) + rep->entries[filled++] = sc->sc_zones[zno]; + } + rep->entries_filled = filled; + mtx_unlock(&sc->sc_lock); + g_io_deliver(bp, 0); + return; + } + case DISK_ZONE_OPEN: + case DISK_ZONE_CLOSE: + case DISK_ZONE_FINISH: + case DISK_ZONE_RWP: { + struct disk_zone_rwp *rwp = &args->zone_params.rwp; + struct bio_queue_head punchq; + struct g_consumer *cp; + bool all = (rwp->flags & DISK_ZONE_RWP_FLAG_ALL) != 0; + bool punch; + + if (all) { + first = 0; + limit = sc->sc_nzones; + } else { + if (rwp->id >= sc->sc_maxlba) { + g_io_deliver(bp, G_ZONED_EXTERR(bp, EINVAL, + "Zone ID past the last LBA. id=%ju" + " maxlba=%ju", (uint64_t)rwp->id, + (uint64_t)sc->sc_maxlba)); + return; + } + if ((rwp->id % sc->sc_zonesecs) != 0) { + g_io_deliver(bp, G_ZONED_EXTERR(bp, EINVAL, + "Zone ID is not the lowest LBA of a zone." + " id=%ju zonesecs=%ju", + (uint64_t)rwp->id, + (uint64_t)sc->sc_zonesecs)); + return; + } + first = g_zoned_zoneno(sc, rwp->id); + limit = first + 1; + } + + /* + * A reset drops the zone's contents, thus hand them back to the + * backing store as long as it is open for writing. + */ + cp = LIST_FIRST(&bp->bio_to->geom->consumer); + punch = args->zone_cmd == DISK_ZONE_RWP && cp->acw > 0; + bioq_init(&punchq); + + mtx_lock(&sc->sc_lock); + sc->sc_zonecmds++; + + /* + * "Open all" must fit within the open-zone limit. + */ + if (all && args->zone_cmd == DISK_ZONE_OPEN && + sc->sc_maxopen != 0) { + uint32_t nclosed = 0; + + for (i = 0; i < sc->sc_nzones; i++) + if (sc->sc_zones[i].zone_condition == + DISK_ZONE_COND_CLOSED) + nclosed++; + if (sc->sc_nopen + nclosed > sc->sc_maxopen) { + mtx_unlock(&sc->sc_lock); + g_zoned_punch_drain(&punchq); + g_io_deliver(bp, G_ZONED_EXTERR(bp, ENOSPC, + "Opening all zones exceeds the open-zone" + " limit. need=%ju maxopen=%ju", + (uint64_t)(sc->sc_nopen + nclosed), + (uint64_t)sc->sc_maxopen)); + return; + } + } + + for (i = first; i < limit; i++) { + struct disk_zone_rep_entry *z = &sc->sc_zones[i]; + + /* + * Conventional zones have no write pointer to manage; + * "all zones" operations skip them, while explicitly + * targeting one is the caller's error. The same goes + * for zones taken readonly or offline. + */ + if (z->zone_type == DISK_ZONE_TYPE_CONVENTIONAL || + z->zone_condition == DISK_ZONE_COND_READONLY || + z->zone_condition == DISK_ZONE_COND_OFFLINE) { + if (all) + continue; + mtx_unlock(&sc->sc_lock); + g_zoned_punch_drain(&punchq); + g_io_deliver(bp, G_ZONED_EXTERR(bp, EINVAL, + "Zone has no write pointer to manage." + " zone=%ju cond=%ju", (uint64_t)i, + (uint64_t)z->zone_condition)); + return; + } + switch (args->zone_cmd) { + /* + * Many operations here are no-op, per ZBC-r06. + */ + case DISK_ZONE_OPEN: + if (all) { + if (z->zone_condition != + DISK_ZONE_COND_CLOSED) + continue; + } else if (z->zone_condition == + DISK_ZONE_COND_FULL || + z->zone_condition == + DISK_ZONE_COND_EXPLICIT_OPEN) + continue; + if (!g_zoned_cond_is_open(z->zone_condition) && + !g_zoned_open_room(sc)) { + mtx_unlock(&sc->sc_lock); + g_zoned_punch_drain(&punchq); + g_io_deliver(bp, G_ZONED_EXTERR(bp, + ENOSPC, "Open-zone limit reached." + " zone=%ju maxopen=%ju", + (uint64_t)i, + (uint64_t)sc->sc_maxopen)); + return; + } + g_zoned_set_cond(sc, z, + DISK_ZONE_COND_EXPLICIT_OPEN); + break; + case DISK_ZONE_CLOSE: + if (!g_zoned_cond_is_open(z->zone_condition)) + continue; + if (z->write_pointer_lba == z->zone_start_lba) + g_zoned_set_cond(sc, z, + DISK_ZONE_COND_EMPTY); + else + g_zoned_set_cond(sc, z, + DISK_ZONE_COND_CLOSED); + break; + case DISK_ZONE_FINISH: + if (all && + !g_zoned_cond_is_open(z->zone_condition) && + z->zone_condition != DISK_ZONE_COND_CLOSED) + continue; + if (z->zone_condition == DISK_ZONE_COND_FULL) + continue; + z->write_pointer_lba = z->zone_start_lba + + z->zone_length; + g_zoned_set_cond(sc, z, DISK_ZONE_COND_FULL); + break; + case DISK_ZONE_RWP: + /* + * Empty zones have nothing to reclaim, but a + * reset still clears the "reset recommended" + * bit that a fault may have left on it. + */ + if (z->zone_condition == DISK_ZONE_COND_EMPTY) { + if ((z->zone_flags & + DISK_ZONE_FLAG_RESET) == 0) + continue; + z->zone_flags &= ~DISK_ZONE_FLAG_RESET; + break; + } + z->write_pointer_lba = z->zone_start_lba; + z->zone_flags &= ~DISK_ZONE_FLAG_RESET; + g_zoned_set_cond(sc, z, DISK_ZONE_COND_EMPTY); + if (punch) + g_zoned_punch_queue(sc, &punchq, z); + break; + } + g_zoned_mark_dirty(sc, i); + } + mtx_unlock(&sc->sc_lock); + if (punch) { + g_zoned_punch_issue(bp, cp, &punchq); + return; + } + g_io_deliver(bp, 0); + return; + } + default: + G_ZONED_LOGREQLVL(1, bp, "Unsupported zone command %u.", + args->zone_cmd); + g_io_deliver(bp, G_ZONED_EXTERR(bp, EOPNOTSUPP, + "Unsupported zone command. cmd=%ju", + (uint64_t)args->zone_cmd)); + return; + } +} + +/* + * Validate a write against the zone model, advancing the write pointer. + */ +static int +g_zoned_write_check(struct g_zoned_softc *sc, struct bio *bp, uint8_t *oldcond) +{ + struct disk_zone_rep_entry *z; + uint64_t lba, end; + uint32_t i, last, zno; + + mtx_assert(&sc->sc_lock, MA_OWNED); + + *oldcond = DISK_ZONE_COND_NOT_WP; + + lba = bp->bio_offset / sc->sc_secsize; + end = (bp->bio_offset + bp->bio_length) / sc->sc_secsize; + if (end > sc->sc_maxlba) + return (G_ZONED_EXTERR(bp, EIO, + "Write past the last LBA. end=%ju maxlba=%ju", + (uint64_t)end, (uint64_t)sc->sc_maxlba)); + + zno = g_zoned_zoneno(sc, lba); + z = &sc->sc_zones[zno]; + + if (z->zone_type == DISK_ZONE_TYPE_CONVENTIONAL) { + /* + * Conventional zones take writes anywhere and requests may + * span zone boundaries into other conventional zones. + */ + last = (end > lba) ? g_zoned_zoneno(sc, end - 1) : zno; + for (i = zno; i <= last; i++) { + if (i > zno && sc->sc_zones[i].zone_type != + DISK_ZONE_TYPE_CONVENTIONAL) { + G_ZONED_LOGREQLVL(1, bp, "Write crosses from " + "a conventional into a sequential zone."); + return (G_ZONED_EXTERR(bp, EIO, + "Write crosses from a conventional into a" + " sequential zone. lba=%ju zone=%ju", + (uint64_t)lba, (uint64_t)i)); + } + if (sc->sc_zones[i].zone_condition == + DISK_ZONE_COND_READONLY || + sc->sc_zones[i].zone_condition == + DISK_ZONE_COND_OFFLINE) { + G_ZONED_LOGREQLVL(1, bp, + "Write to a readonly/offline zone."); + return (G_ZONED_EXTERR(bp, EIO, + "Write to a readonly or offline zone." + " zone=%ju cond=%ju", (uint64_t)i, + (uint64_t)sc->sc_zones[i].zone_condition)); + } + } + } else { + if (z->zone_condition == DISK_ZONE_COND_READONLY || + z->zone_condition == DISK_ZONE_COND_OFFLINE) { + G_ZONED_LOGREQLVL(1, bp, + "Write to a readonly/offline zone."); + return (G_ZONED_EXTERR(bp, EIO, + "Write to a readonly or offline zone." + " zone=%ju cond=%ju", (uint64_t)zno, + (uint64_t)z->zone_condition)); + } + if (end > z->zone_start_lba + z->zone_length) { + G_ZONED_LOGREQLVL(1, bp, + "Write crosses a zone boundary."); + return (G_ZONED_EXTERR(bp, EIO, + "Write crosses a zone boundary. end=%ju" + " zoneend=%ju", (uint64_t)end, + (uint64_t)(z->zone_start_lba + z->zone_length))); + } + + /* + * Sequential-write-required zones accept writes at the + * WP only. + */ + if (z->zone_condition == DISK_ZONE_COND_FULL || + lba != z->write_pointer_lba) { + G_ZONED_LOGREQLVL(1, bp, + "Out-of-order write to zone %u (lba %ju, wp %ju).", + zno, (uintmax_t)lba, + (uintmax_t)z->write_pointer_lba); + return (G_ZONED_EXTERR(bp, EIO, + "Write pointer violation. lba=%ju wp=%ju", + (uint64_t)lba, (uint64_t)z->write_pointer_lba)); + } + + /* + * Implicitly opening one more zone(s) must respect the + * open-zone limit; a write that fills the zone outright + * never leaves it open. + */ + if (!g_zoned_cond_is_open(z->zone_condition) && + end < z->zone_start_lba + z->zone_length && + !g_zoned_open_room(sc)) { + G_ZONED_LOGREQLVL(1, bp, + "Cannot implicitly open zone %u:" + " open-zone limit reached.", zno); + return (G_ZONED_EXTERR(bp, ENOSPC, + "Open-zone limit reached. zone=%ju maxopen=%ju", + (uint64_t)zno, (uint64_t)sc->sc_maxopen)); + } + + *oldcond = z->zone_condition; + z->write_pointer_lba = end; + if (z->write_pointer_lba >= z->zone_start_lba + z->zone_length) + g_zoned_set_cond(sc, z, DISK_ZONE_COND_FULL); + else if (!g_zoned_cond_is_open(z->zone_condition)) + g_zoned_set_cond(sc, z, DISK_ZONE_COND_IMPLICIT_OPEN); + g_zoned_mark_dirty(sc, zno); + } + + return (0); +} + +/* + * Validate a read against the zone model. A read may never reach a zone of + * a different type than the one it starts in, nor touch an offline zone, + * whatever URSWRZ says; only spanning several sequential zones and reading + * above the write pointer depend on it. + */ +static int +g_zoned_read_check(struct g_zoned_softc *sc, struct bio *bp) +{ + struct disk_zone_rep_entry *z; + uint64_t lba, end; + uint32_t i, last, zno; + + mtx_assert(&sc->sc_lock, MA_OWNED); + + lba = bp->bio_offset / sc->sc_secsize; + end = (bp->bio_offset + bp->bio_length) / sc->sc_secsize; + if (end > sc->sc_maxlba) + return (G_ZONED_EXTERR(bp, EIO, + "Read past the last LBA. end=%ju maxlba=%ju", + (uint64_t)end, (uint64_t)sc->sc_maxlba)); + + zno = g_zoned_zoneno(sc, lba); + z = &sc->sc_zones[zno]; + last = (end > lba) ? g_zoned_zoneno(sc, end - 1) : zno; + + for (i = zno; i <= last; i++) { + if (sc->sc_zones[i].zone_condition == DISK_ZONE_COND_OFFLINE) { + G_ZONED_LOGREQLVL(1, bp, "Read from an offline zone."); + return (G_ZONED_EXTERR(bp, EIO, + "Read from an offline zone. zone=%ju", + (uint64_t)i)); + } + if (i > zno && sc->sc_zones[i].zone_type != z->zone_type) { + G_ZONED_LOGREQLVL(1, bp, "Read crosses into a zone of " + "a different type."); + return (G_ZONED_EXTERR(bp, EIO, + "Read crosses into a zone of a different type." + " lba=%ju zone=%ju", (uint64_t)lba, + (uint64_t)i)); + } + } + + if (!sc->sc_rdrestrict || z->zone_type == DISK_ZONE_TYPE_CONVENTIONAL) + return (0); + + if (end > z->zone_start_lba + z->zone_length) { + G_ZONED_LOGREQLVL(1, bp, "Read crosses a zone boundary."); + return (G_ZONED_EXTERR(bp, EIO, + "Read crosses a zone boundary. end=%ju zoneend=%ju", + (uint64_t)end, + (uint64_t)(z->zone_start_lba + z->zone_length))); + } + if (end > z->write_pointer_lba) { + G_ZONED_LOGREQLVL(1, bp, + "Read above the write pointer of zone %u (lba %ju," + " wp %ju).", zno, (uintmax_t)lba, + (uintmax_t)z->write_pointer_lba); + return (G_ZONED_EXTERR(bp, EIO, + "Read above the write pointer. end=%ju wp=%ju", + (uint64_t)end, (uint64_t)z->write_pointer_lba)); + } + return (0); +} + +/* + * A write whose data never reaches the medium leaves the zone claiming blocks + * that were never written, so retract the optimistic write-pointer advance, as + * long as no later write has moved the pointer further. The zone rolls back + * to the condition the write found it in. + */ +static void +g_zoned_write_undo(struct g_zoned_softc *sc, off_t offset, off_t length, + uint8_t oldcond) +{ + struct disk_zone_rep_entry *z; + uint64_t lba, end; + uint32_t zno; + + lba = offset / sc->sc_secsize; + end = (offset + length) / sc->sc_secsize; + + mtx_lock(&sc->sc_lock); + zno = g_zoned_zoneno(sc, lba); + z = &sc->sc_zones[zno]; + if (z->zone_type != DISK_ZONE_TYPE_CONVENTIONAL && + z->write_pointer_lba == end) { + z->write_pointer_lba = lba; + g_zoned_set_cond(sc, z, oldcond); + g_zoned_mark_dirty(sc, zno); + } + mtx_unlock(&sc->sc_lock); +} + +static void +g_zoned_write_done(struct bio *cbp) +{ + struct g_zoned_softc *sc; + struct bio *pbp; + + pbp = cbp->bio_parent; + sc = pbp->bio_to->geom->softc; + if (cbp->bio_error != 0 && sc != NULL) + g_zoned_write_undo(sc, cbp->bio_offset, cbp->bio_length, + (uint8_t)(uintptr_t)cbp->bio_caller1); + g_std_done(cbp); +} + +static void +g_zoned_start(struct bio *bp) +{ + struct g_zoned_softc *sc; + struct g_geom *gp; + struct bio *cbp; + int error; + uint8_t oldcond = DISK_ZONE_COND_NOT_WP; + + gp = bp->bio_to->geom; + sc = gp->softc; + G_ZONED_LOGREQ(bp, "Request received."); + + switch (bp->bio_cmd) { + case BIO_ZONE: + g_zoned_zonecmd(bp, sc); + return; + case BIO_WRITE: + mtx_lock(&sc->sc_lock); + error = g_zoned_write_check(sc, bp, &oldcond); + if (error != 0) { + mtx_unlock(&sc->sc_lock); + g_io_deliver(bp, error); + return; + } + sc->sc_writes++; + sc->sc_wrotebytes += bp->bio_length; + mtx_unlock(&sc->sc_lock); + break; + case BIO_READ: + mtx_lock(&sc->sc_lock); + error = g_zoned_read_check(sc, bp); + if (error != 0) { + mtx_unlock(&sc->sc_lock); + g_io_deliver(bp, error); + return; + } + sc->sc_reads++; + sc->sc_readbytes += bp->bio_length; + mtx_unlock(&sc->sc_lock); + break; + case BIO_FLUSH: + if (g_zoned_flush_begin(sc, gp, bp)) + return; + break; + case BIO_GETATTR: + /* BIO_DELETE is refused below, don't advertise it. */ + if (g_handleattr_int(bp, "GEOM::candelete", 0)) + return; + break; + case BIO_DELETE: + /* + * Zoned device have no unmap; sequential zones are reclaimed by + * resetting the write pointer instead. + */ + g_io_deliver(bp, G_ZONED_EXTERR(bp, EOPNOTSUPP, + "Zoned providers have no unmap; reset the write pointer" + " instead.")); + return; + default: + break; + } + + cbp = g_clone_bio(bp); + if (cbp == NULL) { + if (bp->bio_cmd == BIO_WRITE) + g_zoned_write_undo(sc, bp->bio_offset, bp->bio_length, + oldcond); + g_io_deliver(bp, ENOMEM); + return; + } + if (bp->bio_cmd == BIO_WRITE) { + cbp->bio_done = g_zoned_write_done; + cbp->bio_caller1 = (void *)(uintptr_t)oldcond; + } else + cbp->bio_done = g_std_done; + G_ZONED_LOGREQ(cbp, "Sending request."); + g_io_request(cbp, LIST_FIRST(&gp->consumer)); +} + +static int +g_zoned_access(struct g_provider *pp __unused, int dr __unused, + int dw __unused, int de __unused) +{ + + return (0); +} + +static struct g_geom * +g_zoned_create(struct g_class *mp, const struct g_zoned_metadata *md, + struct g_provider *pp) +{ + struct g_zoned_softc *sc; + struct g_geom *gp; + struct g_provider *newpp; + struct g_consumer *cp; + char name[64]; + uint64_t nzones, zonesecs; + uint32_t i; + int error; + + g_topology_assert(); + + nzones = g_zoned_nzones(pp->mediasize, md->md_zonesize, pp->sectorsize); + zonesecs = md->md_zonesize / pp->sectorsize; + if (nzones == 0 || zonesecs == 0 || zonesecs > G_ZONED_MAXZONESECS || + (md->md_zonesize % pp->sectorsize) != 0 || + md->md_nconv > G_ZONED_MAXCONV || + (md->md_flags & ~G_ZONED_MD_FLAGSMASK) != 0) { + G_ZONED_DEBUG(0, "Bogus metadata on %s.", pp->name); + return (NULL); + } + for (i = 0; i < md->md_nconv; i++) { + if (md->md_conv[i].cr_count == 0 || + md->md_conv[i].cr_first >= nzones || + md->md_conv[i].cr_count > + nzones - md->md_conv[i].cr_first) { + G_ZONED_DEBUG(0, + "Bogus conventional zone range on %s.", pp->name); + return (NULL); + } + } + + snprintf(name, sizeof(name), "%s%s", pp->name, G_ZONED_SUFFIX); + LIST_FOREACH(gp, &mp->geom, geom) { + if (strcmp(gp->name, name) == 0) { + G_ZONED_DEBUG(0, "Device %s already exists.", name); + return (NULL); + } + } + + gp = g_new_geom(mp, name); + sc = g_malloc(sizeof(*sc), M_WAITOK | M_ZERO); + + sc->sc_id = md->md_id; + sc->sc_zonesize = md->md_zonesize; + sc->sc_secsize = pp->sectorsize; + sc->sc_zonesecs = zonesecs; + sc->sc_nzones = (uint32_t)nzones; + sc->sc_maxlba = nzones * zonesecs; + sc->sc_nconv = md->md_nconv; + sc->sc_maxopen = md->md_maxopen; + sc->sc_rdrestrict = (md->md_flags & G_ZONED_MD_RESTRICTED_READS) != 0; + bcopy(md->md_conv, sc->sc_conv, sizeof(sc->sc_conv)); + /* Count per zone so that overlapping ranges are not double-counted. */ + for (i = 0; i < nzones; i++) + if (g_zoned_is_conv(sc, i)) + sc->sc_convzones++; + sc->sc_zones = malloc(nzones * sizeof(*sc->sc_zones), M_ZONED, + M_NOWAIT | M_ZERO); + if (sc->sc_zones == NULL) { + G_ZONED_DEBUG(0, "Cannot allocate a %u zone table for %s.", + sc->sc_nzones, pp->name); + g_free(sc); + g_destroy_geom(gp); + return (NULL); + } + sc->sc_tabsecs = 1 + + howmany(nzones * G_ZONED_ENTRY_SIZE, pp->sectorsize); + sc->sc_taboff = (pp->mediasize - pp->sectorsize) - + (off_t)sc->sc_tabsecs * pp->sectorsize; + g_zoned_reset_zones(sc); + mtx_init(&sc->sc_lock, "gzoned lock", NULL, MTX_DEF); + gp->softc = sc; + + newpp = g_new_providerf(gp, "%s", gp->name); + newpp->flags |= G_PF_DIRECT_SEND | G_PF_DIRECT_RECEIVE; + newpp->mediasize = (off_t)nzones * md->md_zonesize; + newpp->sectorsize = pp->sectorsize; + newpp->stripesize = pp->stripesize; + newpp->stripeoffset = pp->stripeoffset; + + cp = g_new_consumer(gp); + cp->flags |= G_CF_DIRECT_SEND | G_CF_DIRECT_RECEIVE; + error = g_attach(cp, pp); + if (error != 0) { + G_ZONED_DEBUG(0, "Cannot attach to provider %s.", pp->name); + goto fail; + } + /* Hold the backing provider open as long as the device exists. */ + error = g_access(cp, 1, 1, 1); + if (error != 0) { + G_ZONED_DEBUG(0, "Cannot access provider %s.", pp->name); + goto fail; + } + + /* Restore the persistent zone state or initialise an empty table. */ + g_zoned_load_table(sc, cp); + + newpp->flags |= pp->flags & G_PF_ACCEPT_UNMAPPED; + g_error_provider(newpp, 0); + G_ZONED_DEBUG(0, "Device %s created (%u zones of %jd bytes).", + gp->name, sc->sc_nzones, (intmax_t)sc->sc_zonesize); + return (gp); +fail: + if (cp->provider != NULL) + g_detach(cp); + g_destroy_consumer(cp); + g_destroy_provider(newpp); + g_destroy_geom(gp); + return (NULL); +} + +static int +g_zoned_read_metadata(struct g_consumer *cp, struct g_zoned_metadata *md) +{ + struct g_provider *pp; + u_char *buf; + int error; + + g_topology_assert(); + + error = g_access(cp, 1, 0, 0); + if (error != 0) + return (error); + pp = cp->provider; + g_topology_unlock(); + buf = g_read_data(cp, pp->mediasize - pp->sectorsize, pp->sectorsize, + &error); + g_topology_lock(); + g_access(cp, -1, 0, 0); + if (buf == NULL) + return (error); + zoned_metadata_decode(buf, md); + g_free(buf); + return (0); +} + +static struct g_geom * +g_zoned_taste(struct g_class *mp, struct g_provider *pp, int flags __unused) +{ + struct g_zoned_metadata md; + struct g_zoned_softc *sc; + struct g_consumer *cp; + struct g_geom *gp; + int error; + + g_topology_assert(); + + /* Skip providers that are already open for writing. */ + if (pp->acw > 0) + return (NULL); + + G_ZONED_DEBUG(3, "Tasting %s.", pp->name); + + gp = g_new_geom(mp, "zoned:taste"); + cp = g_new_consumer(gp); + cp->flags |= G_CF_DIRECT_SEND | G_CF_DIRECT_RECEIVE; + error = g_attach(cp, pp); + if (error == 0) { + error = g_zoned_read_metadata(cp, &md); + g_detach(cp); + } + g_destroy_consumer(cp); + g_destroy_geom(gp); + if (error != 0) + return (NULL); + + if (strcmp(md.md_magic, G_ZONED_MAGIC) != 0) + return (NULL); + if (md.md_version > G_ZONED_VERSION) { + printf("geom_zoned.ko module is too old to handle %s.\n", + pp->name); + return (NULL); + } + if (md.md_provsize != (uint64_t)pp->mediasize) + return (NULL); + if (md.md_sectorsize != pp->sectorsize) + return (NULL); + + /* Already running? */ + LIST_FOREACH(gp, &mp->geom, geom) { + sc = gp->softc; + if (sc != NULL && sc->sc_id == md.md_id) + return (NULL); + } + + gp = g_zoned_create(mp, &md, pp); + if (gp == NULL) + G_ZONED_DEBUG(0, "Cannot create zoned device on %s.", + pp->name); + return (gp); +} + +static int +g_zoned_destroy(struct g_geom *gp, boolean_t force) +{ + struct g_zoned_softc *sc; + struct g_provider *pp; + + g_topology_assert(); + sc = gp->softc; + if (sc == NULL) + return (ENXIO); + /* An orphaned provider and a stop request can both land here. */ + if ((gp->flags & G_GEOM_WITHER) != 0) + return (ENXIO); + pp = LIST_FIRST(&gp->provider); + if (pp != NULL && (pp->acr != 0 || pp->acw != 0 || pp->ace != 0)) { + if (force) { + G_ZONED_DEBUG(0, + "Device %s is still open, so it " + "can't be definitely removed.", + pp->name); + } else { + G_ZONED_DEBUG(1, "Device %s is still open (r%dw%de%d).", + pp->name, pp->acr, pp->acw, pp->ace); + return (EBUSY); + } + } else { + G_ZONED_DEBUG(0, "Device %s removed.", gp->name); + } + + /* Commit unflushed zone state to survive the stop. */ + g_zoned_flush_sync(sc, LIST_FIRST(&gp->consumer)); + + /* + * Give up the reference taken when the device was created, so that + * withering can reap the consumer. + */ + g_wither_geom_close(gp, ENXIO); + return (0); +} + +static int +g_zoned_destroy_geom(struct gctl_req *req __unused, + struct g_class *mp __unused, struct g_geom *gp) +{ + + return (g_zoned_destroy(gp, 0)); +} + +static void +g_zoned_orphan(struct g_consumer *cp) +{ + + g_topology_assert(); + g_zoned_destroy(cp->geom, 1); +} + +/* + * The metadata and zone table live at the tail of the backing provider, which + * has just moved: neither the on-disk state nor the zone layout can stay + * consistent, so tear the device down. Unflushed zone state is discarded rather + * than written to what is no longer the table's location. + */ +static void +g_zoned_resize(struct g_consumer *cp) +{ + struct g_zoned_softc *sc; + + g_topology_assert(); + + sc = cp->geom->softc; + if (sc == NULL) + return; + G_ZONED_DEBUG(0, "Provider %s resized; destroying %s.", + cp->provider->name, cp->geom->name); + mtx_lock(&sc->sc_lock); + sc->sc_dirty = false; + sc->sc_hdrdirty = false; + mtx_unlock(&sc->sc_lock); + g_zoned_destroy(cp->geom, 1); +} + +static void +g_zoned_providergone(struct g_provider *pp) +{ + struct g_geom *gp = pp->geom; + struct g_zoned_softc *sc = gp->softc; + + gp->softc = NULL; + free(sc->sc_zones, M_ZONED); + mtx_destroy(&sc->sc_lock); + g_free(sc); +} + +static struct g_geom * +g_zoned_find_geom(struct g_class *mp, const char *name) +{ + struct g_geom *gp; + + if (strncmp(name, _PATH_DEV, strlen(_PATH_DEV)) == 0) + name += strlen(_PATH_DEV); + + LIST_FOREACH(gp, &mp->geom, geom) { + if (gp->softc != NULL && strcmp(gp->name, name) == 0) + return (gp); + } + return (NULL); +} + +static void +g_zoned_ctl_create(struct gctl_req *req, struct g_class *mp) +{ + struct g_zoned_metadata md; + struct g_provider *pp; + struct g_consumer *cp; + struct g_geom *gp; + const char *name; + u_char *sector, *buf; + int *nargs, error, len; + + g_topology_assert(); + + nargs = gctl_get_paraml(req, "nargs", sizeof(*nargs)); + if (nargs == NULL) { + gctl_error(req, "No '%s' argument.", "nargs"); + return; + } + if (*nargs != 1) { + gctl_error(req, "Missing device."); + return; + } + name = gctl_get_asciiparam(req, "arg0"); + if (name == NULL) { + gctl_error(req, "No 'arg0' argument."); + return; + } + if (strncmp(name, _PATH_DEV, strlen(_PATH_DEV)) == 0) + name += strlen(_PATH_DEV); + pp = g_provider_by_name(name); + if (pp == NULL) { + gctl_error(req, "No such provider: %s.", name); + return; + } + sector = gctl_get_param(req, "metadata", &len); + if (sector == NULL) { + gctl_error(req, "No '%s' argument.", "metadata"); + return; + } + if (len < (int)sizeof(struct g_zoned_metadata) || + len > (int)pp->sectorsize) { + gctl_error(req, "Invalid '%s' argument.", "metadata"); + return; + } + + /* + * The geometry was measured in userland, before the request reached + * the topology lock, so make sure the provider still matches it. + */ + zoned_metadata_decode(sector, &md); + if (memcmp(md.md_magic, G_ZONED_MAGIC, sizeof(G_ZONED_MAGIC)) != 0 || + md.md_version != G_ZONED_VERSION || + md.md_provsize != (uint64_t)pp->mediasize || + md.md_sectorsize != pp->sectorsize) { + gctl_error(req, "Invalid metadata for %s.", name); + return; + } + + gp = g_zoned_create(mp, &md, pp); + if (gp == NULL) { + gctl_error(req, "Cannot create a zoned device on %s.", name); + return; + } + + /* The tail sector is ours now; pad the metadata out to fill it. */ + cp = LIST_FIRST(&gp->consumer); + buf = g_malloc(pp->sectorsize, M_WAITOK | M_ZERO); + memcpy(buf, sector, len); + g_topology_unlock(); + error = g_write_data(cp, pp->mediasize - pp->sectorsize, buf, + pp->sectorsize); + if (error == 0) + error = g_io_flush(cp); + g_topology_lock(); + g_free(buf); + if (error != 0) { + gctl_error(req, "Cannot store metadata on %s (error=%d).", + name, error); + (void)g_zoned_destroy(gp, 1); + } +} + +static void +g_zoned_ctl_destroy(struct gctl_req *req, struct g_class *mp) +{ + struct g_geom *gp; + const char *name; + char param[16]; + int *force, *nargs, error, i; + + g_topology_assert(); + + nargs = gctl_get_paraml(req, "nargs", sizeof(*nargs)); + if (nargs == NULL) { + gctl_error(req, "No '%s' argument.", "nargs"); + return; + } + if (*nargs <= 0) { + gctl_error(req, "Missing device(s)."); + return; + } + force = gctl_get_paraml(req, "force", sizeof(*force)); + if (force == NULL) { + gctl_error(req, "No '%s' argument.", "force"); + return; + } + + for (i = 0; i < *nargs; i++) { + snprintf(param, sizeof(param), "arg%d", i); + name = gctl_get_asciiparam(req, param); + if (name == NULL) { + gctl_error(req, "No 'arg%d' argument.", i); + return; + } + gp = g_zoned_find_geom(mp, name); + if (gp == NULL) { + gctl_error(req, "No such device: %s.", name); + return; + } + error = g_zoned_destroy(gp, *force); + if (error != 0) { + gctl_error(req, "Cannot destroy device %s (error=%d).", + gp->name, error); + return; + } + } +} + +/* + * Fault injection: force a zone readonly or offline and flag it as needing a + * write-pointer reset, or clear the fault again. The state persists through the + * zone table. + */ +static void +g_zoned_ctl_fault(struct gctl_req *req, struct g_class *mp) +{ + struct g_zoned_softc *sc; + struct g_geom *gp; + struct disk_zone_rep_entry *z; + const char *name, *state; + intmax_t *zone; + uint32_t zno; + + g_topology_assert(); + + name = gctl_get_asciiparam(req, "arg0"); + if (name == NULL) { + gctl_error(req, "Missing device."); + return; + } + zone = gctl_get_paraml(req, "zone", sizeof(*zone)); + if (zone == NULL) { + gctl_error(req, "No '%s' argument.", "zone"); + return; + } + state = gctl_get_asciiparam(req, "state"); + if (state == NULL) { + gctl_error(req, "No '%s' argument.", "state"); + return; + } + gp = g_zoned_find_geom(mp, name); + if (gp == NULL) { + gctl_error(req, "No such device: %s.", name); + return; + } + sc = gp->softc; + if (*zone < 0 || (uintmax_t)*zone >= sc->sc_nzones) { + gctl_error(req, "Zone %jd out of range.", *zone); + return; + } + zno = (uint32_t)*zone; + + mtx_lock(&sc->sc_lock); + z = &sc->sc_zones[zno]; + if (strcmp(state, "ro") == 0 || strcmp(state, "readonly") == 0) + g_zoned_set_cond(sc, z, DISK_ZONE_COND_READONLY); + else if (strcmp(state, "offline") == 0) + g_zoned_set_cond(sc, z, DISK_ZONE_COND_OFFLINE); + else if (strcmp(state, "reset") == 0) + z->zone_flags |= DISK_ZONE_FLAG_RESET; + else if (strcmp(state, "clear") == 0) { + /* Reobtain the normal condition from the write pointer. */ + z->zone_flags &= ~DISK_ZONE_FLAG_RESET; + if (z->zone_type == DISK_ZONE_TYPE_CONVENTIONAL) + g_zoned_set_cond(sc, z, DISK_ZONE_COND_NOT_WP); + else if (z->write_pointer_lba == z->zone_start_lba) + g_zoned_set_cond(sc, z, DISK_ZONE_COND_EMPTY); + else if (z->write_pointer_lba == + z->zone_start_lba + z->zone_length) + g_zoned_set_cond(sc, z, DISK_ZONE_COND_FULL); + else + g_zoned_set_cond(sc, z, DISK_ZONE_COND_CLOSED); + } else { + mtx_unlock(&sc->sc_lock); + gctl_error(req, "Invalid state '%s'.", state); + return; + } + g_zoned_mark_dirty(sc, zno); + mtx_unlock(&sc->sc_lock); +} + +static void +g_zoned_config(struct gctl_req *req, struct g_class *mp, const char *verb) +{ + uint32_t *version; + + g_topology_assert(); + + version = gctl_get_paraml(req, "version", sizeof(*version)); + if (version == NULL) { + gctl_error(req, "No '%s' argument.", "version"); + return; + } + if (*version != G_ZONED_VERSION) { + gctl_error(req, "Userland and kernel parts are out of sync."); + return; + } + + if (strcmp(verb, "create") == 0) { + g_zoned_ctl_create(req, mp); + return; + } + if (strcmp(verb, "destroy") == 0 || strcmp(verb, "stop") == 0) { + g_zoned_ctl_destroy(req, mp); + return; + } + if (strcmp(verb, "fault") == 0) { + g_zoned_ctl_fault(req, mp); + return; + } + gctl_error(req, "Unknown verb."); +} + +static void +g_zoned_dumpconf(struct sbuf *sb, const char *indent, struct g_geom *gp, + struct g_consumer *cp, struct g_provider *pp) +{ + struct g_zoned_softc *sc; + + if (pp != NULL || cp != NULL) + return; + sc = gp->softc; + sbuf_printf(sb, "%s%jd\n", indent, + (intmax_t)sc->sc_zonesize); + sbuf_printf(sb, "%s%u\n", indent, sc->sc_nzones); + sbuf_printf(sb, "%s%u\n", + indent, sc->sc_convzones); + sbuf_printf(sb, "%sHost Managed\n", indent); + sbuf_printf(sb, "%s%s\n", indent, + sc->sc_rdrestrict ? "No" : "Yes"); + sbuf_printf(sb, "%s%u\n", indent, + sc->sc_maxopen); + sbuf_printf(sb, "%s%u\n", indent, sc->sc_nopen); + sbuf_printf(sb, "%s%ju\n", indent, sc->sc_reads); + sbuf_printf(sb, "%s%ju\n", indent, sc->sc_writes); + sbuf_printf(sb, "%s%ju\n", indent, + sc->sc_readbytes); + sbuf_printf(sb, "%s%ju\n", indent, + sc->sc_wrotebytes); + sbuf_printf(sb, "%s%ju\n", indent, + sc->sc_zonecmds); +} + +DECLARE_GEOM_CLASS(g_zoned_class, g_zoned); +MODULE_VERSION(geom_zoned, 0); diff --git a/sys/geom/zoned/g_zoned.h b/sys/geom/zoned/g_zoned.h new file mode 100644 index 000000000000..ee19b39aea74 --- /dev/null +++ b/sys/geom/zoned/g_zoned.h @@ -0,0 +1,291 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2026 voidanix + */ + +#ifndef _G_ZONED_H_ +#define _G_ZONED_H_ + +#include + +#define G_ZONED_CLASS_NAME "ZONED" +#define G_ZONED_MAGIC "GEOM::ZONED" +#define G_ZONED_TABLE_MAGIC "GEOM::ZONEDTBL" +/* Appended to the backing provider's name to name the zoned provider. */ +#define G_ZONED_SUFFIX ".zoned" + +/* + * Version history: + * 1 - Initial version number. + */ +#define G_ZONED_VERSION 1 + +/* + * Sentinel for "unlimited open/active sequential zones". + * See SVPD_ZBDC_MAX_SEQ_UNLIMITED. + */ +#define G_ZONED_SEQ_UNLIMITED 0xffffffff + +/* + * On-disk layout, carved out of the end of the backing provider: + * + * [ ... usable (zoned) region ... | table header | zone entries | metadata ] + * (1 sector) (N entries) (last sec) + * + * Table header + zone entries hold the live per-zone state and are maintained + * entirely by the kernel. + */ +/* On-disk zone-state table header, at the start of the table's first sector. */ +struct g_zoned_table_hdr { + char th_magic[16]; /* G_ZONED_TABLE_MAGIC. */ + uint32_t th_version; /* Version number. */ + uint32_t th_nzones; /* Zone entries that follow. */ +}; +_Static_assert(sizeof(struct g_zoned_table_hdr) == 24, + "on-disk zone table header layout changed"); + +/* On-disk zone-state table entry. */ +struct g_zoned_disk_entry { + uint8_t de_type; /* Zone type. */ + uint8_t de_condition; /* Zone condition. */ + uint8_t de_flags; /* Zone attribute flags. */ + uint8_t de_reserved; + uint32_t de_write_pointer; /* WP offset in the zone. */ +}; +#define G_ZONED_ENTRY_SIZE (sizeof(struct g_zoned_disk_entry)) +_Static_assert(G_ZONED_ENTRY_SIZE == 8, "on-disk zone entry layout changed"); + +/* + * write_pointer_lba for zones that have no valid write pointer. Report all + * ones across the whole 64-bit field, like a ZBC drive might. + */ +#define G_ZONED_WP_NONE_LBA 0xffffffffffffffff +/* On-disk, zone-relative counterpart in de_write_pointer. */ +#define G_ZONED_WP_NONE 0xffffffff +/* + * Zone size cap (in sectors) keeping every valid zone-relative write pointer, + * including that of a full zone (== zone length), below G_ZONED_WP_NONE. + */ +#define G_ZONED_MAXZONESECS (G_ZONED_WP_NONE - 1) + +/* + * Maximum number of conventional-zone ranges that fit in the metadata. + */ +#define G_ZONED_MAXCONV 16 + +/* Maximum number of zones for a single device. */ +#define G_ZONED_MAXZONES UINT32_MAX + +/* + * A run of consecutive conventional (non-sequential-write) zones. ZBC/ZAC + * identify zones by their start LBA, a 64-bit field; zone indices get the same + * width. + */ +struct g_zoned_convrange { + uint64_t cr_first; /* First zone of the range. */ + uint64_t cr_count; /* Number of zones in the range. */ +}; + +/* md_flags bits. */ +#define G_ZONED_MD_RESTRICTED_READS 0x00000001 /* URSWRZ off. */ +#define G_ZONED_MD_FLAGSMASK G_ZONED_MD_RESTRICTED_READS + +struct g_zoned_metadata { + char md_magic[16]; /* Magic value. */ + uint32_t md_version; /* Version number. */ + uint32_t md_id; /* Unique ID. */ + uint64_t md_zonesize; /* Zone size in bytes. */ + uint32_t md_sectorsize; /* Provider sector size in bytes. */ + uint32_t md_nconv; /* Conventional ranges in use. */ + uint64_t md_provsize; /* Provider size in bytes. */ + struct g_zoned_convrange md_conv[G_ZONED_MAXCONV]; + uint32_t md_flags; /* G_ZONED_MD_* flags. */ + uint32_t md_maxopen; /* Open zone limit, 0 = unlimited. */ +}; +_Static_assert(sizeof(struct g_zoned_metadata) == 312, + "on-disk metadata layout changed"); +_Static_assert(__offsetof(struct g_zoned_metadata, md_conv) == 48, + "on-disk metadata layout changed"); + +/* + * The on-disk image has the same layout as the struct, so marshalling is a + * matter of byte order. The image is copied in and out rather than cast to, + * as the buffers holding it are only guaranteed to be byte aligned. + */ +static __inline void +zoned_metadata_encode(const struct g_zoned_metadata *md, u_char *data) +{ + struct g_zoned_metadata d; + u_int i; + + memset(&d, 0, sizeof(d)); + memcpy(d.md_magic, md->md_magic, sizeof(d.md_magic)); + d.md_version = htole32(md->md_version); + d.md_id = htole32(md->md_id); + d.md_zonesize = htole64(md->md_zonesize); + d.md_sectorsize = htole32(md->md_sectorsize); + d.md_nconv = htole32(md->md_nconv); + d.md_provsize = htole64(md->md_provsize); + for (i = 0; i < G_ZONED_MAXCONV; i++) { + d.md_conv[i].cr_first = htole64(md->md_conv[i].cr_first); + d.md_conv[i].cr_count = htole64(md->md_conv[i].cr_count); + } + d.md_flags = htole32(md->md_flags); + d.md_maxopen = htole32(md->md_maxopen); + memcpy(data, &d, sizeof(d)); +} + +static __inline void +zoned_metadata_decode(const u_char *data, struct g_zoned_metadata *md) +{ + struct g_zoned_metadata d; + u_int i; + + memcpy(&d, data, sizeof(d)); + memcpy(md->md_magic, d.md_magic, sizeof(md->md_magic)); + md->md_version = le32toh(d.md_version); + md->md_id = le32toh(d.md_id); + md->md_zonesize = le64toh(d.md_zonesize); + md->md_sectorsize = le32toh(d.md_sectorsize); + md->md_nconv = le32toh(d.md_nconv); + md->md_provsize = le64toh(d.md_provsize); + for (i = 0; i < G_ZONED_MAXCONV; i++) { + md->md_conv[i].cr_first = le64toh(d.md_conv[i].cr_first); + md->md_conv[i].cr_count = le64toh(d.md_conv[i].cr_count); + } + md->md_flags = le32toh(d.md_flags); + md->md_maxopen = le32toh(d.md_maxopen); +} + +/* + * Number of zones a provider of a given geometry can hold after reserving room + * at the tail for the metadata sector and the zone-state table. Returns 0 if + * the zone size leaves room for no zone at all, or if it yields more zones + * than G_ZONED_MAXZONES. + */ +static __inline uint32_t +g_zoned_nzones(off_t mediasize, off_t zonesize, u_int secsize) +{ + uint64_t nmax, nzones, reserve; + + if (zonesize <= 0 || secsize == 0 || mediasize <= 0) + return (0); + /* Over-reserve using the zone count that ignores the reservation. */ + nmax = (uint64_t)mediasize / (uint64_t)zonesize; + if (nmax == 0) + return (0); + /* Metadata sector + table header sector + sector-aligned entries. */ + reserve = 2 * (uint64_t)secsize + + roundup2(nmax * G_ZONED_ENTRY_SIZE, (uint64_t)secsize); + if ((uint64_t)mediasize <= reserve) + return (0); + nzones = ((uint64_t)mediasize - reserve) / (uint64_t)zonesize; + /* + * Reject rather than truncate, as wrapped counts can quietly describe + * devices of the wrong size. Note that nzones must be wider than the + * bound for this to work. + */ + if (nzones > G_ZONED_MAXZONES) + return (0); + return ((uint32_t)nzones); +} + +#ifdef _KERNEL +#define G_ZONED_DEBUG(lvl, ...) \ + _GEOM_DEBUG("GEOM_ZONED", g_zoned_debug, (lvl), NULL, __VA_ARGS__) +#define G_ZONED_LOGREQLVL(lvl, bp, ...) \ + _GEOM_DEBUG("GEOM_ZONED", g_zoned_debug, (lvl), (bp), __VA_ARGS__) +#define G_ZONED_LOGREQ(bp, ...) G_ZONED_LOGREQLVL(2, bp, __VA_ARGS__) + +/* + * Live state of one emulated zoned device. The sc_zones array mirrors the + * on-disk zone-state table; dirty entries are re-encoded and written out + * lazily on BIO_FLUSH. Zone states are kept in RAM. + */ +struct g_zoned_softc { + struct mtx sc_lock; + uint32_t sc_id; /* Unique ID. */ + off_t sc_zonesize; /* Zone bytes. */ + u_int sc_secsize; /* Sector bytes. */ + uint64_t sc_zonesecs; /* Zone sectors. */ + uint32_t sc_nzones; /* Number of zones. */ + uint64_t sc_maxlba; /* Last LBA + 1. */ + uint32_t sc_nconv; /* Conv. ranges. */ + uint32_t sc_convzones; /* Conv. zones. */ + uint32_t sc_maxopen; /* 0 = unlimited. */ + uint32_t sc_nopen; /* Open zones. */ + bool sc_rdrestrict; /* URSWRZ off. */ + struct g_zoned_convrange sc_conv[G_ZONED_MAXCONV]; + struct disk_zone_rep_entry *sc_zones; /* sc_nzones long. */ + /* Zone-state table placement and dirty tracking. */ + uint32_t sc_tabsecs; /* Table sectors. */ + off_t sc_taboff; /* Table offset. */ + bool sc_dirty; /* Pending writes. */ + bool sc_hdrdirty; /* Header dirty. */ + uint32_t sc_dirtylo; /* First dirty zone. */ + uint32_t sc_dirtyhi; /* Last dirty zone. */ + /* Statistics. */ + uintmax_t sc_reads; + uintmax_t sc_writes; + uintmax_t sc_readbytes; + uintmax_t sc_wrotebytes; + uintmax_t sc_zonecmds; +}; + +static __inline void +zoned_table_hdr_encode(const struct g_zoned_table_hdr *th, u_char *data) +{ + struct g_zoned_table_hdr d; + + memset(&d, 0, sizeof(d)); + memcpy(d.th_magic, th->th_magic, sizeof(d.th_magic)); + d.th_version = htole32(th->th_version); + d.th_nzones = htole32(th->th_nzones); + memcpy(data, &d, sizeof(d)); +} + +static __inline void +zoned_table_hdr_decode(const u_char *data, struct g_zoned_table_hdr *th) +{ + struct g_zoned_table_hdr d; + + memcpy(&d, data, sizeof(d)); + memcpy(th->th_magic, d.th_magic, sizeof(th->th_magic)); + th->th_version = le32toh(d.th_version); + th->th_nzones = le32toh(d.th_nzones); +} + +static __inline void +g_zoned_entry_encode(const struct disk_zone_rep_entry *z, u_char *data) +{ + struct g_zoned_disk_entry de; + + de.de_type = z->zone_type; + de.de_condition = z->zone_condition; + de.de_flags = z->zone_flags; + de.de_reserved = 0; + de.de_write_pointer = htole32( + z->write_pointer_lba == G_ZONED_WP_NONE_LBA ? G_ZONED_WP_NONE : + (uint32_t)(z->write_pointer_lba - z->zone_start_lba)); + memcpy(data, &de, sizeof(de)); +} + +static __inline void +g_zoned_entry_decode(const u_char *data, struct disk_zone_rep_entry *z, + uint64_t start_lba) +{ + struct g_zoned_disk_entry de; + uint32_t wp; + + memcpy(&de, data, sizeof(de)); + z->zone_type = de.de_type; + z->zone_condition = de.de_condition; + z->zone_flags = de.de_flags; + wp = le32toh(de.de_write_pointer); + z->write_pointer_lba = (wp == G_ZONED_WP_NONE) ? G_ZONED_WP_NONE_LBA : + start_lba + wp; +} +#endif /* _KERNEL */ + +#endif /* _G_ZONED_H_ */ diff --git a/sys/modules/geom/Makefile b/sys/modules/geom/Makefile index 7169c639e6bd..0d8bafa4084f 100644 --- a/sys/modules/geom/Makefile +++ b/sys/modules/geom/Makefile @@ -21,7 +21,8 @@ SUBDIR= geom_cache \ geom_union \ geom_uzip \ geom_virstor \ - geom_zero + geom_zero \ + geom_zoned .if ${MK_CCD} != "no" || defined(ALL_MODULES) SUBDIR+= geom_ccd diff --git a/sys/modules/geom/geom_zoned/Makefile b/sys/modules/geom/geom_zoned/Makefile new file mode 100644 index 000000000000..b36c4d7e4877 --- /dev/null +++ b/sys/modules/geom/geom_zoned/Makefile @@ -0,0 +1,6 @@ +.PATH: ${SRCTOP}/sys/geom/zoned + +KMOD= geom_zoned +SRCS= g_zoned.c + +.include diff --git a/sys/sys/disk_zone.h b/sys/sys/disk_zone.h index 731fc04ecc29..c234e7ec18dd 100644 --- a/sys/sys/disk_zone.h +++ b/sys/sys/disk_zone.h @@ -45,9 +45,9 @@ /* * There are currently three types of zoned devices: - * + * * Drive Managed: - * Drive Managed drives look and act just like a standard random access + * Drive Managed drives look and act just like a standard random access * block device, but underneath, the drive reads and writes the bulk of * its capacity using SMR zones. Sequential writes will yield better * performance, but writing sequentially is not required. @@ -58,7 +58,7 @@ * is not required to manage the zones on the drive, though. Sequential * writes will yield better performance in Sequential Write Preferred * zones, but the host can write randomly in those zones. - * + * * Host Managed: * Host Managed drives expose the underlying zone layout via SCSI or ATA * commands. The host is required to access the zones according to the @@ -130,15 +130,27 @@ struct disk_zone_rep_entry { #define DISK_ZONE_COND_IMPLICIT_OPEN 0x02 #define DISK_ZONE_COND_EXPLICIT_OPEN 0x03 #define DISK_ZONE_COND_CLOSED 0x04 +#define DISK_ZONE_COND_INACTIVE 0x05 #define DISK_ZONE_COND_READONLY 0x0D #define DISK_ZONE_COND_FULL 0x0E #define DISK_ZONE_COND_OFFLINE 0x0F uint8_t zone_flags; #define DISK_ZONE_FLAG_RESET 0x01 /* Zone needs RWP */ -#define DISK_ZONE_FLAG_NON_SEQ 0x02 /* Zone accssessed nonseq */ +#define DISK_ZONE_FLAG_NON_SEQ 0x02 /* Zone accessed nonseq */ uint64_t zone_length; uint64_t zone_start_lba; uint64_t write_pointer_lba; + /* + * Only zone conditions that have a write pointer report one + * here; ZBC and ZAC leave the field undefined otherwise, and + * drives disagree on what they put in it. + */ +#define DISK_ZONE_WP_INVALID(cond) \ + ((cond) == DISK_ZONE_COND_NOT_WP || \ + (cond) == DISK_ZONE_COND_INACTIVE || \ + (cond) == DISK_ZONE_COND_READONLY || \ + (cond) == DISK_ZONE_COND_FULL || \ + (cond) == DISK_ZONE_COND_OFFLINE) /* XXX KDM padding space may not be a good idea inside the bio */ uint8_t reserved[32]; }; diff --git a/sys/sys/exterr_cat.h b/sys/sys/exterr_cat.h index 21655e16faa7..235238a562ec 100644 --- a/sys/sys/exterr_cat.h +++ b/sys/sys/exterr_cat.h @@ -57,5 +57,6 @@ #define EXTERR_CAT_HWPMC_AMD 20 #define EXTERR_CAT_HWPMC_MOD 21 #define EXTERR_CAT_TTY 22 +#define EXTERR_CAT_GEOMZONED 23 #endif diff --git a/tests/sys/geom/class/Makefile b/tests/sys/geom/class/Makefile index c58ebc0f72a0..58b0183ab8a5 100644 --- a/tests/sys/geom/class/Makefile +++ b/tests/sys/geom/class/Makefile @@ -20,7 +20,9 @@ TESTS_SUBDIRS+= stripe TESTS_SUBDIRS+= union TESTS_SUBDIRS+= uzip TESTS_SUBDIRS+= virstor +TESTS_SUBDIRS+= zoned ${PACKAGE}FILES+= geom_subr.sh +${PACKAGE}FILES+= zoned_subr.sh .include diff --git a/tests/sys/geom/class/zoned/Makefile b/tests/sys/geom/class/zoned/Makefile new file mode 100644 index 000000000000..e61a629a96cb --- /dev/null +++ b/tests/sys/geom/class/zoned/Makefile @@ -0,0 +1,9 @@ +PACKAGE= tests + +TESTSDIR= ${TESTSBASE}/sys/geom/class/${.CURDIR:T} + +ATF_TESTS_SH+= zoned_test + +${PACKAGE}FILES+= conf.sh + +.include diff --git a/tests/sys/geom/class/zoned/conf.sh b/tests/sys/geom/class/zoned/conf.sh new file mode 100644 index 000000000000..01d9f0bd45d4 --- /dev/null +++ b/tests/sys/geom/class/zoned/conf.sh @@ -0,0 +1,59 @@ +#!/bin/sh + +class="zoned" + +gzoned_test_setup() +{ + geom_atf_test_setup +} + +# Print the write pointer LBA of the zone containing the given LBA. +zone_wp() +{ + zoned_zone_wp /dev/${md}.zoned $1 +} + +# Print the start LBA of the given zone number. +zone_start() +{ + zoned_zone_start /dev/${md}.zoned $1 +} + +# Print the number of zones matching a report option. +zone_count() +{ + zoned_zone_count /dev/${md}.zoned $1 +} + +wait_dev() +{ + for _i in $(seq 1 50); do + [ -c "$1" ] && return 0 + sleep 0.2 + done + atf_fail "$1 did not appear" +} + +wait_dev_gone() +{ + for _i in $(seq 1 50); do + [ -c "$1" ] || return 0 + sleep 0.2 + done + atf_fail "$1 did not go away" +} + +gzoned_test_cleanup() +{ + if [ -f "$TEST_MDS_FILE" ]; then + while read md; do + [ -c /dev/${md}.zoned ] && \ + gzoned destroy $md.zoned 2>/dev/null + mdconfig -d -u $md 2>/dev/null + done < $TEST_MDS_FILE + fi + true +} + +. `dirname $0`/../geom_subr.sh +. `dirname $0`/../zoned_subr.sh diff --git a/tests/sys/geom/class/zoned/zoned_test.sh b/tests/sys/geom/class/zoned/zoned_test.sh new file mode 100644 index 000000000000..1d772d9b6a9d --- /dev/null +++ b/tests/sys/geom/class/zoned/zoned_test.sh @@ -0,0 +1,1056 @@ +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2026 voidanix +# + +. $(atf_get_srcdir)/conf.sh + +atf_test_case create cleanup +create_head() +{ + atf_set "descr" "Basic gzoned device creation" + atf_set "require.user" "root" +} +create_body() +{ + gzoned_test_setup + + zoned_backing_md + atf_check gzoned create -s 256m ${md} + atf_check test -c /dev/${md}.zoned +} +create_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case is_zoned cleanup +is_zoned_head() +{ + atf_set "descr" "gzoned creates a host-managed zoned device" + atf_set "require.user" "root" +} +is_zoned_body() +{ + gzoned_test_setup + + zoned_backing_md + atf_check gzoned create ${md} + atf_check_equal "Host Managed" \ + "$(zonectl -d /dev/${md}.zoned -c params | \ + awk -F': ' '/Zone Mode/ {print $2}')" +} +is_zoned_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case conventional cleanup +conventional_head() +{ + atf_set "descr" "gzoned device has a few conventional zones" + atf_set "require.user" "root" +} +conventional_body() +{ + gzoned_test_setup + + zoned_backing_md + atf_check gzoned create -s 256m -c 1,3 ${md} + atf_check_equal "2" "$(zone_count nonwp)" +} +conventional_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case conventional_single cleanup +conventional_single_head() +{ + atf_set "descr" "gzoned create -c accepts a single zone" + atf_set "require.user" "root" +} +conventional_single_body() +{ + gzoned_test_setup + + zoned_backing_md + atf_check gzoned create -s 256m -c 2 ${md} + atf_check_equal "1" "$(zone_count nonwp)" + # The conventional zone is zone 2. At LBA 512m / 512 = 0x100000. + atf_check_equal "0x100000" \ + "$(zonectl -d /dev/${md}.zoned -c rz -o nonwp -P script | \ + awk -F',' 'NR == 1 {gsub(/ /, "", $1); print $1}')" +} +conventional_single_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case conventional_range cleanup +conventional_range_head() +{ + atf_set "descr" "gzoned device has a range of conventional zones" + atf_set "require.user" "root" +} +conventional_range_body() +{ + gzoned_test_setup + + zoned_backing_md + atf_check gzoned create -s 256m -c 0-2 ${md} + atf_check_equal "3" "$(zone_count nonwp)" +} +conventional_range_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case all_zones_same cleanup +all_zones_same_head() +{ + atf_set "descr" "gzoned device has all zones of the same type" + atf_set "require.user" "root" +} +all_zones_same_body() +{ + gzoned_test_setup + + zoned_backing_md + atf_check gzoned create -s 256m -c 0-3 ${md} + atf_check -o match:"^4 zones," \ + -o match:"Zone lengths and types are all the same" \ + zonectl -d /dev/${md}.zoned -c rz -P summary +} +all_zones_same_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case conventional_ranges cleanup +conventional_ranges_head() +{ + atf_set "descr" "gzoned create -c accepts a list of zone ranges" + atf_set "require.user" "root" +} +conventional_ranges_body() +{ + gzoned_test_setup + + zoned_backing_md + atf_check gzoned create -s 256m -c 0,2-3 ${md} + atf_check_equal "3" "$(zone_count nonwp)" + # Zone 1 alone stays sequential: the second conventional zone + # reported must be zone 2, at LBA 512m / 512 = 0x100000. + atf_check_equal "0x100000" \ + "$(zonectl -d /dev/${md}.zoned -c rz -o nonwp -P script | \ + awk -F',' 'NR == 2 {gsub(/ /, "", $1); print $1}')" +} +conventional_ranges_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case zone_size cleanup +zone_size_head() +{ + atf_set "descr" "gzoned create -s adjusts the zone size" + atf_set "require.user" "root" +} +zone_size_body() +{ + gzoned_test_setup + + zoned_backing_md + atf_check gzoned create -s 128m ${md} + # 1024m of zone space now holds eight 128m zones instead of four. + atf_check -o match:"^8 zones," \ + zonectl -d /dev/${md}.zoned -c rz -P summary + # Zone 1 starts at LBA 128m / 512 = 0x40000. + atf_check_equal "0x40000" "$(zone_start 1)" +} +zone_size_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case zone_count_derived cleanup +zone_count_derived_head() +{ + atf_set "descr" "gzoned obtains the same zone count at every taste" + atf_set "require.user" "root" +} +zone_count_derived_body() +{ + gzoned_test_setup + + zoned_backing_md + atf_check gzoned create -s 128m ${md} + atf_check -o match:"^8 zones," \ + zonectl -d /dev/${md}.zoned -c rz -P summary + last_start=$(zone_start 7) + + atf_check gzoned stop ${md}.zoned + wait_dev_gone /dev/${md}.zoned + true > /dev/${md} + wait_dev /dev/${md}.zoned + atf_check -o match:"^8 zones," \ + zonectl -d /dev/${md}.zoned -c rz -P summary + atf_check_equal "${last_start}" "$(zone_start 7)" +} +zone_count_derived_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case create_readonly cleanup +create_readonly_head() +{ + atf_set "descr" "gzoned refuses a provider it cannot write to" + atf_set "require.user" "root" +} +create_readonly_body() +{ + gzoned_test_setup + + atf_check truncate -s 1025m backing_file + attach_md md -t vnode -f backing_file -o readonly + atf_check -s not-exit:0 -e ignore gzoned create -s 256m ${md} + atf_check test ! -c /dev/${md}.zoned +} +create_readonly_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case zone_size_too_large cleanup +zone_size_too_large_head() +{ + atf_set "descr" "gzoned refuses a zone size larger than the provider" + atf_set "require.user" "root" +} +zone_size_too_large_body() +{ + gzoned_test_setup + + zoned_backing_md + atf_check -s not-exit:0 -e match:"unusable" \ + gzoned create -s 2g ${md} + atf_check test ! -c /dev/${md}.zoned +} +zone_size_too_large_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case zone_size_too_many cleanup +zone_size_too_many_head() +{ + atf_set "descr" "gzoned refuses a zone size yielding too many zones" + atf_set "require.user" "root" +} +zone_size_too_many_body() +{ + gzoned_test_setup + + # One zone per sector on a sparse 8T provider overflows the 32-bit zone + # count. + atf_check truncate -s 8t backing_file + attach_md md -t vnode -f backing_file + atf_check -s not-exit:0 -e match:"unusable" \ + gzoned create -s 512 ${md} + atf_check test ! -c /dev/${md}.zoned +} +zone_size_too_many_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case reset_wp cleanup +reset_wp_head() +{ + atf_set "descr" \ + "gzoned write pointer can be reset inside a seq-write-req zone" + atf_set "require.user" "root" +} +reset_wp_body() +{ + gzoned_test_setup + + zoned_backing_md + atf_check gzoned create -s 256m ${md} + + # Advance the write pointer of zone 0 by 1m (2048 512-byte LBAs). + atf_check -e ignore \ + dd if=/dev/zero of=/dev/${md}.zoned bs=1m count=1 + atf_check_equal "0x800" "$(zone_wp 0)" + + atf_check zonectl -d /dev/${md}.zoned -c rwp -l 0 + atf_check_equal "0" "$(zone_wp 0)" +} +reset_wp_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case reset_wp_conv cleanup +reset_wp_conv_head() +{ + atf_set "descr" \ + "gzoned write pointer cannot be reset inside a conventional zone" + atf_set "require.user" "root" +} +reset_wp_conv_body() +{ + gzoned_test_setup + + zoned_backing_md + atf_check gzoned create -s 256m -c 0 ${md} + atf_check test -c /dev/${md}.zoned + atf_check_equal "1" "$(zone_count nonwp)" + atf_check -s not-exit:0 -e ignore \ + zonectl -d /dev/${md}.zoned -c rwp -l 0 +} +reset_wp_conv_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case reset_wp_empty cleanup +reset_wp_empty_head() +{ + atf_set "descr" \ + "gzoned accepts a write pointer reset of an already-empty zone" + atf_set "require.user" "root" +} +reset_wp_empty_body() +{ + gzoned_test_setup + + zoned_backing_md + atf_check gzoned create -s 256m ${md} + atf_check_equal "0" "$(zone_wp 0)" + atf_check zonectl -d /dev/${md}.zoned -c rwp -l 0 + atf_check_equal "0" "$(zone_wp 0)" + atf_check_equal "4" "$(zone_count empty)" +} +reset_wp_empty_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case reset_wp_finished cleanup +reset_wp_finished_head() +{ + atf_set "descr" "gzoned resets a finished zone back to empty" + atf_set "require.user" "root" +} +reset_wp_finished_body() +{ + gzoned_test_setup + + zoned_backing_md + atf_check gzoned create -s 256m ${md} + atf_check zonectl -d /dev/${md}.zoned -c finish -l 0 + atf_check_equal "1" "$(zone_count full)" + + atf_check zonectl -d /dev/${md}.zoned -c rwp -l 0 + atf_check_equal "0" "$(zone_count full)" + atf_check_equal "0" "$(zone_wp 0)" + atf_check_equal "4" "$(zone_count empty)" +} +reset_wp_finished_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case reset_wp_all_readonly cleanup +reset_wp_all_readonly_head() +{ + atf_set "descr" \ + "gzoned keeps the contents of a zone reset all did not reset" + atf_set "require.user" "root" +} +reset_wp_all_readonly_body() +{ + gzoned_test_setup + + zoned_backing_md + atf_check gzoned create -s 256m ${md} + + # Fill the start of zone 1 with something unrecognizable. + atf_check -e ignore dd if=/dev/random of=pattern bs=1m count=1 + atf_check -e ignore \ + dd if=pattern of=/dev/${md}.zoned bs=1m oseek=256 count=1 + atf_check gzoned fault -z 1 -s ro ${md}.zoned + + atf_check zonectl -d /dev/${md}.zoned -c rwp -a + atf_check_equal "1" "$(zone_count ro)" + # Read-only zone reports no write pointer. Clear the fault to see + # whether the pointer was left where it was beforehand. + atf_check gzoned fault -z 1 -s clear ${md}.zoned + atf_check_equal "0x80800" "$(zone_wp 524288)" + atf_check -e ignore \ + dd if=/dev/${md}.zoned of=after bs=1m iseek=256 count=1 + atf_check cmp pattern after +} +reset_wp_all_readonly_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case zone_id_lowest_lba cleanup +zone_id_lowest_lba_head() +{ + atf_set "descr" \ + "gzoned only takes a zone ID that is a zone's lowest LBA" + atf_set "require.user" "root" +} +zone_id_lowest_lba_body() +{ + gzoned_test_setup + + zoned_backing_md + atf_check gzoned create -s 256m ${md} + + atf_check zonectl -d /dev/${md}.zoned -c rwp -l 0x80000 + atf_check -s not-exit:0 -e ignore \ + zonectl -d /dev/${md}.zoned -c rwp -l 0x80001 + atf_check -s not-exit:0 -e ignore \ + zonectl -d /dev/${md}.zoned -c open -l 1 + atf_check -s not-exit:0 -e ignore \ + zonectl -d /dev/${md}.zoned -c finish -l 0x7ffff + atf_check_equal "4" "$(zone_count empty)" + + atf_check zonectl -d /dev/${md}.zoned -c rwp -a +} +zone_id_lowest_lba_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case unrestricted_reads cleanup +unrestricted_reads_head() +{ + atf_set "descr" \ + "gzoned device allows reads above the write pointer by default" + atf_set "require.user" "root" +} +unrestricted_reads_body() +{ + gzoned_test_setup + + zoned_backing_md + atf_check gzoned create -s 256m ${md} + atf_check -o match:"URSWRZ.*: Yes" \ + zonectl -d /dev/${md}.zoned -c params + # Reading an empty sequential zone is fine. + atf_check -e ignore \ + dd if=/dev/${md}.zoned of=/dev/null bs=512 count=1 + # So is one 768k read straddling the 256m mark, as in write_boundary: + # spanning zones of the same type is what URSWRZ lifts. + atf_check -e ignore \ + dd if=/dev/${md}.zoned of=/dev/null bs=768k iseek=341 count=1 +} +unrestricted_reads_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case read_zone_types cleanup +read_zone_types_head() +{ + atf_set "descr" \ + "Reads may not cross between zone types even when unrestricted" + atf_set "require.user" "root" +} +read_zone_types_body() +{ + gzoned_test_setup + + zoned_backing_md + atf_check gzoned create -s 256m -c 0 ${md} + atf_check -o match:"URSWRZ.*: Yes" \ + zonectl -d /dev/${md}.zoned -c params + + # The 768k read straddling the 256m mark reaches the kernel as one + # bio, running from a conventional zone into the sequential. + atf_check -s not-exit:0 -e ignore \ + dd if=/dev/${md}.zoned of=/dev/null bs=768k iseek=341 count=1 + # Preceding block lies within the conventional zone: it's fine. + atf_check -e ignore \ + dd if=/dev/${md}.zoned of=/dev/null bs=768k iseek=340 count=1 +} +read_zone_types_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case restricted_reads cleanup +restricted_reads_head() +{ + atf_set "descr" \ + "gzoned -u device rejects reads beyond the write pointer" + atf_set "require.user" "root" +} +restricted_reads_body() +{ + gzoned_test_setup + + zoned_backing_md + atf_check gzoned create -s 256m -u ${md} + atf_check -o match:"URSWRZ.*: No" \ + zonectl -d /dev/${md}.zoned -c params + + # Reading an empty sequential zone must fail... + atf_check -s not-exit:0 -e ignore \ + dd if=/dev/${md}.zoned of=/dev/null bs=512 count=1 + + # ...until data has been written. + atf_check -e ignore dd if=/dev/zero of=/dev/${md}.zoned bs=1m count=1 + atf_check -e ignore \ + dd if=/dev/${md}.zoned of=/dev/null bs=1m count=1 + atf_check -s not-exit:0 -e ignore \ + dd if=/dev/${md}.zoned of=/dev/null bs=1m count=2 +} +restricted_reads_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case write_at_wp cleanup +write_at_wp_head() +{ + atf_set "descr" "gzoned accepts writes at the write pointer" + atf_set "require.user" "root" +} +write_at_wp_body() +{ + gzoned_test_setup + + zoned_backing_md + atf_check gzoned create -s 256m ${md} + atf_check -e ignore dd if=/dev/zero of=/dev/${md}.zoned bs=1m count=1 + atf_check_equal "0x800" "$(zone_wp 0)" + # A second write at the advanced write pointer is accepted too. + atf_check -e ignore \ + dd if=/dev/zero of=/dev/${md}.zoned bs=1m oseek=1 count=1 + atf_check_equal "0x1000" "$(zone_wp 0)" +} +write_at_wp_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case write_out_of_order cleanup +write_out_of_order_head() +{ + atf_set "descr" "gzoned rejects writes that skip the write pointer" + atf_set "require.user" "root" +} +write_out_of_order_body() +{ + gzoned_test_setup + + zoned_backing_md + atf_check gzoned create -s 256m ${md} + # Zone 0 write pointer is at LBA 0; writing at 1m shall fail. + atf_check -s not-exit:0 -e ignore \ + dd if=/dev/zero of=/dev/${md}.zoned bs=1m oseek=1 count=1 + atf_check_equal "0" "$(zone_wp 0)" +} +write_out_of_order_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case write_boundary cleanup +write_boundary_head() +{ + atf_set "descr" "gzoned rejects writes crossing a zone boundary" + atf_set "require.user" "root" +} +write_boundary_body() +{ + gzoned_test_setup + + zoned_backing_md + atf_check gzoned create -s 256m -c 0 ${md} + # One 768k write straddling the 256m mark (768k does not divide + # 256m, so the request reaches the kernel as a single bio going + # from the conventional zone 0 into the sequential zone 1). + atf_check -s not-exit:0 -e ignore \ + dd if=/dev/zero of=/dev/${md}.zoned bs=768k oseek=341 count=1 + # The preceding block lies within the conventional zone and is fine. + atf_check -e ignore \ + dd if=/dev/zero of=/dev/${md}.zoned bs=768k oseek=340 count=1 +} +write_boundary_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case write_conv_boundary cleanup +write_conv_boundary_head() +{ + atf_set "descr" \ + "gzoned allows writes crossing two conventional zones" + atf_set "require.user" "root" +} +write_conv_boundary_body() +{ + gzoned_test_setup + + zoned_backing_md + atf_check gzoned create -s 256m -c 0-1 ${md} + # The same straddling write rejected in write_boundary is fine + # when the zones on both sides are conventional. + atf_check -e ignore \ + dd if=/dev/zero of=/dev/${md}.zoned bs=768k oseek=341 count=1 +} +write_conv_boundary_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case write_conv_anywhere cleanup +write_conv_anywhere_head() +{ + atf_set "descr" "gzoned conventional zones accept writes at any offset" + atf_set "require.user" "root" +} +write_conv_anywhere_body() +{ + gzoned_test_setup + + zoned_backing_md + atf_check gzoned create -s 256m -c 0 ${md} + # No write pointer: writing far into the zone and then again before + # the previous write must both succeed, and the zone never opens. + atf_check -e ignore \ + dd if=/dev/zero of=/dev/${md}.zoned bs=1m oseek=10 count=1 + atf_check -e ignore \ + dd if=/dev/zero of=/dev/${md}.zoned bs=1m oseek=2 count=1 + atf_check_equal "0" "$(zone_count imp_open)" +} +write_conv_anywhere_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case write_full cleanup +write_full_head() +{ + atf_set "descr" "gzoned rejects writes to a full zone" + atf_set "require.user" "root" +} +write_full_body() +{ + gzoned_test_setup + + zoned_backing_md + atf_check gzoned create -s 256m ${md} + atf_check zonectl -d /dev/${md}.zoned -c finish -l 0 + atf_check_equal "1" "$(zone_count full)" + atf_check -s not-exit:0 -e ignore \ + dd if=/dev/zero of=/dev/${md}.zoned bs=1m count=1 +} +write_full_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case write_closed_zone cleanup +write_closed_zone_head() +{ + atf_set "descr" "gzoned writes to a closed zone implicitly reopen it" + atf_set "require.user" "root" +} +write_closed_zone_body() +{ + gzoned_test_setup + + zoned_backing_md + atf_check gzoned create -s 256m ${md} + atf_check -e ignore dd if=/dev/zero of=/dev/${md}.zoned bs=1m count=1 + atf_check zonectl -d /dev/${md}.zoned -c close -l 0 + atf_check_equal "1" "$(zone_count closed)" + + atf_check -e ignore \ + dd if=/dev/zero of=/dev/${md}.zoned bs=1m oseek=1 count=1 + atf_check_equal "0" "$(zone_count closed)" + atf_check_equal "1" "$(zone_count imp_open)" +} +write_closed_zone_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case persistence cleanup +persistence_head() +{ + atf_set "descr" "gzoned zone state survives a stop and re-taste" + atf_set "require.user" "root" +} +persistence_body() +{ + gzoned_test_setup + + zoned_backing_md + atf_check gzoned create -s 256m ${md} + atf_check -e ignore dd if=/dev/zero of=/dev/${md}.zoned bs=1m count=1 + atf_check_equal "0x800" "$(zone_wp 0)" + + # Stopping will commit the zone state; re-tasting the backing provider + # brings the device back with its write pointers restored. + atf_check gzoned stop ${md}.zoned + wait_dev_gone /dev/${md}.zoned + true > /dev/${md} + wait_dev /dev/${md}.zoned + atf_check_equal "0x800" "$(zone_wp 0)" +} +persistence_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case clear_clears_metadata cleanup +clear_clears_metadata_head() +{ + atf_set "descr" "gzoned clear erases the on-disk metadata" + atf_set "require.user" "root" +} +clear_clears_metadata_body() +{ + gzoned_test_setup + + zoned_backing_md + atf_check gzoned create -s 256m ${md} + # The backing provider is held open for as long as the device exists, + # so its metadata can only be cleared once the device is gone. + atf_check -s not-exit:0 -e ignore gzoned clear /dev/${md} + atf_check gzoned stop ${md}.zoned + wait_dev_gone /dev/${md}.zoned + atf_check gzoned clear /dev/${md} + true > /dev/${md} + sleep 1 + atf_check test ! -c /dev/${md}.zoned +} +clear_clears_metadata_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case max_open cleanup +max_open_head() +{ + atf_set "descr" "gzoned enforces the open-zone limit" + atf_set "require.user" "root" +} +max_open_body() +{ + gzoned_test_setup + + zoned_backing_md + atf_check gzoned create -s 256m -m 1 ${md} + atf_check -o match:"Open Sequential Write Required Zones: 1" \ + zonectl -d /dev/${md}.zoned -c params + + # A write into a second zone implicitly closes the first. + atf_check -e ignore dd if=/dev/zero of=/dev/${md}.zoned bs=1m count=1 + atf_check -e ignore \ + dd if=/dev/zero of=/dev/${md}.zoned bs=1m oseek=256 count=1 + atf_check_equal "1" "$(zone_count closed)" + atf_check_equal "1" "$(zone_count imp_open)" + + # Explicitly opening zone 0 closes the implicitly open zone 1; a second + # explicit open will exceed the limit. + atf_check zonectl -d /dev/${md}.zoned -c open -l 0 + atf_check_equal "1" "$(zone_count exp_open)" + atf_check -s not-exit:0 -e ignore \ + zonectl -d /dev/${md}.zoned -c open -l 0x80000 +} +max_open_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case max_open_limit cleanup +max_open_limit_head() +{ + atf_set "descr" "gzoned rejects explicit opens beyond the open limit" + atf_set "require.user" "root" +} +max_open_limit_body() +{ + gzoned_test_setup + + zoned_backing_md + atf_check gzoned create -s 256m -m 2 ${md} + atf_check zonectl -d /dev/${md}.zoned -c open -l 0 + atf_check zonectl -d /dev/${md}.zoned -c open -l 0x80000 + atf_check_equal "2" "$(zone_count exp_open)" + + # Explicitly open zones cannot be implicitly closed. A third open + # exceeds the limit until one of them is closed. + atf_check -s not-exit:0 -e ignore \ + zonectl -d /dev/${md}.zoned -c open -l 0x100000 + atf_check zonectl -d /dev/${md}.zoned -c close -l 0 + atf_check zonectl -d /dev/${md}.zoned -c open -l 0x100000 + atf_check_equal "2" "$(zone_count exp_open)" +} +max_open_limit_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case stop_force cleanup +stop_force_head() +{ + atf_set "descr" "gzoned stop -f stops a busy device" + atf_set "require.user" "root" +} +stop_force_body() +{ + gzoned_test_setup + + zoned_backing_md + atf_check gzoned create -s 256m ${md} + + # Keep the device open; a plain stop must fail, a forced one work. + sleep 5 < /dev/${md}.zoned & + spid=$! + sleep 0.5 + atf_check -s not-exit:0 -e ignore gzoned stop ${md}.zoned + atf_check gzoned stop -f ${md}.zoned + wait_dev_gone /dev/${md}.zoned + kill $spid 2>/dev/null + wait $spid 2>/dev/null || true +} +stop_force_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case create_on_zoned cleanup +create_on_zoned_head() +{ + atf_set "descr" \ + "gzoned create on an already-zoned device fails gracefully" + atf_set "require.user" "root" +} +create_on_zoned_body() +{ + gzoned_test_setup + + zoned_backing_md + atf_check gzoned create -s 256m ${md} + # The metadata of the second device cannot land in the sequential zones + # of the first; creation must be refused and leave the first device + # intact, with all zones still empty. + atf_check -s not-exit:0 -e match:"host-managed" \ + gzoned create -s 256m ${md}.zoned + atf_check test -c /dev/${md}.zoned + atf_check -o match:"^4 zones," \ + zonectl -d /dev/${md}.zoned -c rz -P summary + atf_check_equal "4" "$(zone_count empty)" +} +create_on_zoned_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case fault_injection cleanup +fault_injection_head() +{ + atf_set "descr" "gzoned faulted zones reject writes until cleared" + atf_set "require.user" "root" +} +fault_injection_body() +{ + gzoned_test_setup + + zoned_backing_md + atf_check gzoned create -s 256m ${md} + + atf_check gzoned fault -z 1 -s ro ${md}.zoned + atf_check_equal "1" "$(zone_count ro)" + atf_check -s not-exit:0 -e ignore \ + dd if=/dev/zero of=/dev/${md}.zoned bs=1m oseek=256 count=1 + + atf_check gzoned fault -z 1 -s clear ${md}.zoned + atf_check_equal "0" "$(zone_count ro)" + atf_check -e ignore \ + dd if=/dev/zero of=/dev/${md}.zoned bs=1m oseek=256 count=1 + + atf_check gzoned fault -z 2 -s offline ${md}.zoned + atf_check_equal "1" "$(zone_count offline)" + # A read only zone stays readable; an offline one does not. + atf_check gzoned fault -z 1 -s ro ${md}.zoned + atf_check -e ignore \ + dd if=/dev/${md}.zoned of=/dev/null bs=1m iseek=256 count=1 + atf_check -s not-exit:0 -e ignore \ + dd if=/dev/${md}.zoned of=/dev/null bs=1m iseek=512 count=1 + atf_check gzoned fault -z 1 -s clear ${md}.zoned + atf_check gzoned fault -z 3 -s reset ${md}.zoned + atf_check_equal "1" "$(zone_count reset)" +} +fault_injection_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case reset_wp_clears_fault cleanup +reset_wp_clears_fault_head() +{ + atf_set "descr" "gzoned reset write pointer clears an injected reset fault" + atf_set "require.user" "root" +} +reset_wp_clears_fault_body() +{ + gzoned_test_setup + + zoned_attach_md + + # Freshly created zone is already empty: the injected fault is the only + # thing a reset has to clear. + atf_check gzoned fault -z 3 -s reset ${md}.zoned + atf_check_equal "1" "$(zone_count reset)" + atf_check zonectl -d /dev/${md}.zoned -c rwp -l $(zone_start 3) + atf_check_equal "0" "$(zone_count reset)" + + # ALso holds when every zone is reset at once. + atf_check gzoned fault -z 3 -s reset ${md}.zoned + atf_check_equal "1" "$(zone_count reset)" + atf_check zonectl -d /dev/${md}.zoned -c rwp -a + atf_check_equal "0" "$(zone_count reset)" +} +reset_wp_clears_fault_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case delete_rejected cleanup +delete_rejected_head() +{ + atf_set "descr" "gzoned rejects BIO_DELETE, hides TRIM support" + atf_set "require.user" "root" +} +delete_rejected_body() +{ + gzoned_test_setup + + zoned_backing_md + atf_check gzoned create -s 256m ${md} + atf_check_equal "No" \ + "$(diskinfo -v /dev/${md}.zoned | \ + awk '/TRIM\/UNMAP support/ {print $1}')" + atf_check -s not-exit:0 -e ignore -o ignore \ + trim -f /dev/${md}.zoned +} +delete_rejected_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case conventional_overlap cleanup +conventional_overlap_head() +{ + atf_set "descr" "gzoned create -c refuses overlapping zone ranges" + atf_set "require.user" "root" +} +conventional_overlap_body() +{ + gzoned_test_setup + + zoned_backing_md + atf_check -s not-exit:0 -e match:"overlaps" \ + gzoned create -s 256m -c 0-2,1-3 ${md} + atf_check test ! -c /dev/${md}.zoned + atf_check -s not-exit:0 -e match:"overlaps" \ + gzoned create -s 256m -c 1,1 ${md} + atf_check -s not-exit:0 -e match:"overlaps" \ + gzoned create -s 256m -c 0-3,2 ${md} + atf_check gzoned create -s 256m -c 0-1,2-3 ${md} + atf_check_equal "4" "$(zone_count nonwp)" +} +conventional_overlap_cleanup() +{ + gzoned_test_cleanup +} + +atf_test_case resize cleanup +resize_head() +{ + atf_set "descr" "Resizing the backing provider destroys the device" + atf_set "require.user" "root" +} +resize_body() +{ + gzoned_test_setup + + zoned_backing_md + atf_check gzoned create -s 256m ${md} + atf_check test -c /dev/${md}.zoned + + # The zone layout is fixed at creation, with the metadata sitting at the + # end of the provider: neither survives the provider changing size. The + # device is destroyed rather than left describing a disk that is no + # longer there. + atf_check truncate -s 2049m backing_file + atf_check mdconfig -r -u ${md} -s 2049m + wait_dev_gone /dev/${md}.zoned + + # Stale metadata is out of reach of the taste, at its new size: the + # device does not come back on its own. + true > /dev/${md} + sleep 1 + atf_check test ! -c /dev/${md}.zoned +} +resize_cleanup() +{ + gzoned_test_cleanup +} + +atf_init_test_cases() +{ + atf_add_test_case create + atf_add_test_case create_on_zoned + atf_add_test_case is_zoned + atf_add_test_case conventional + atf_add_test_case conventional_single + atf_add_test_case conventional_range + atf_add_test_case conventional_ranges + atf_add_test_case conventional_overlap + atf_add_test_case all_zones_same + atf_add_test_case zone_size + atf_add_test_case zone_count_derived + atf_add_test_case create_readonly + atf_add_test_case zone_size_too_large + atf_add_test_case zone_size_too_many + atf_add_test_case reset_wp + atf_add_test_case reset_wp_conv + atf_add_test_case reset_wp_empty + atf_add_test_case reset_wp_finished + atf_add_test_case reset_wp_all_readonly + atf_add_test_case zone_id_lowest_lba + atf_add_test_case unrestricted_reads + atf_add_test_case read_zone_types + atf_add_test_case restricted_reads + atf_add_test_case write_at_wp + atf_add_test_case write_out_of_order + atf_add_test_case write_boundary + atf_add_test_case write_conv_boundary + atf_add_test_case write_conv_anywhere + atf_add_test_case write_full + atf_add_test_case write_closed_zone + atf_add_test_case persistence + atf_add_test_case clear_clears_metadata + atf_add_test_case max_open + atf_add_test_case max_open_limit + atf_add_test_case stop_force + atf_add_test_case fault_injection + atf_add_test_case reset_wp_clears_fault + atf_add_test_case delete_rejected + atf_add_test_case resize +} diff --git a/tests/sys/geom/class/zoned_subr.sh b/tests/sys/geom/class/zoned_subr.sh new file mode 100644 index 000000000000..8f7e6590c6d6 --- /dev/null +++ b/tests/sys/geom/class/zoned_subr.sh @@ -0,0 +1,43 @@ +#!/bin/sh + +# Helpers for exercising GEOM classes on top of a gzoned provider. Source this +# in addition to geom_subr.sh. + +# Create a vnode-backed md large enough for four 256m zones. gzoned reserves +# space at the end of the provider for its metadata block and zone table, so an +# exact multiple of the zone size (1024m) would lose a zone. +zoned_backing_md() +{ + atf_check truncate -s 1025m backing_file + attach_md md -t vnode -f backing_file +} + +# Set up a gzoned provider on a fresh backing md. Any argument is passed to +# gzoned create as its conventional zone specification. +zoned_attach_md() +{ + local conv=$1 + + zoned_backing_md + atf_check gzoned create -s 256m ${conv:+-c ${conv}} ${md} +} + +# Number of zones on $1 matching the report option $2, e.g. "nonwp". +zoned_zone_count() +{ + zonectl -d $1 -c rz -o $2 -P summary | awk '/zones,/ {print $1}' +} + +# Write pointer LBA of the zone containing LBA $2 on $1. +zoned_zone_wp() +{ + zonectl -d $1 -c rz -l $2 -P script | \ + awk -F',' 'NR == 1 {gsub(/ /, "", $3); print $3}' +} + +# Start LBA of zone number $2 on $1. +zoned_zone_start() +{ + zonectl -d $1 -c rz -P script | \ + awk -F',' -v n=$(($2 + 1)) 'NR == n {gsub(/ /, "", $1); print $1}' +} diff --git a/usr.sbin/zonectl/Makefile b/usr.sbin/zonectl/Makefile index 69915ec4916c..9df61d0c0ab9 100644 --- a/usr.sbin/zonectl/Makefile +++ b/usr.sbin/zonectl/Makefile @@ -1,8 +1,12 @@ +.include + PROG= zonectl SRCS= zonectl.c SDIR= ${SRCTOP}/sys LIBADD= cam sbuf util MAN= zonectl.8 -CFLAGS+=-g -O0 + +HAS_TESTS= +SUBDIR.${MK_TESTS}+= tests .include diff --git a/usr.sbin/zonectl/tests/Makefile b/usr.sbin/zonectl/tests/Makefile new file mode 100644 index 000000000000..38d09905c923 --- /dev/null +++ b/usr.sbin/zonectl/tests/Makefile @@ -0,0 +1,5 @@ +PACKAGE= tests + +ATF_TESTS_SH= zonectl_test + +.include diff --git a/usr.sbin/zonectl/tests/zonectl_test.sh b/usr.sbin/zonectl/tests/zonectl_test.sh new file mode 100644 index 000000000000..844b9576ebf3 --- /dev/null +++ b/usr.sbin/zonectl/tests/zonectl_test.sh @@ -0,0 +1,221 @@ +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2026 voidanix +# + +MD_DEV_FILE="md.dev" + +alloc_zoned_md() +{ + atf_check truncate -s 1025m backing_file + md=$(mdconfig -a -t vnode -f backing_file) || \ + atf_fail "mdconfig -a failed" + echo ${md} > $MD_DEV_FILE +} + +# Print the number of zones matching a report option. +zone_count() +{ + zonectl -d /dev/${md}.zoned -c rz -o $1 -P summary | \ + awk '/zones,/ {print $1}' +} + +zoned_md_cleanup() +{ + if [ -f "$MD_DEV_FILE" ]; then + read md < $MD_DEV_FILE + [ -c /dev/${md}.zoned ] && \ + gzoned destroy ${md}.zoned 2>/dev/null + mdconfig -d -u ${md} 2>/dev/null + fi + true +} + +atf_test_case report_filter cleanup +report_filter_head() +{ + atf_set "descr" "zonectl -o only reports zones matching the filter" + atf_set "require.user" "root" +} +report_filter_body() +{ + alloc_zoned_md + atf_check gzoned create -s 256m -c 0-1 ${md} + + atf_check_equal "2" \ + "$(zonectl -d /dev/${md}.zoned -c rz -o nonwp -P summary | \ + awk '/zones,/ {print $1}')" + atf_check_equal "2" \ + "$(zonectl -d /dev/${md}.zoned -c rz -o empty -P summary | \ + awk '/zones,/ {print $1}')" + atf_check_equal "0" \ + "$(zonectl -d /dev/${md}.zoned -c rz -o full -P summary | \ + awk '/zones,/ {print $1}')" +} +report_filter_cleanup() +{ + zoned_md_cleanup +} + +atf_test_case report_zones cleanup +report_zones_head() +{ + atf_set "descr" "zonectl -c rz reports every zone" + atf_set "require.user" "root" +} +report_zones_body() +{ + alloc_zoned_md + atf_check gzoned create -s 256m ${md} + atf_check -o match:"^4 zones," \ + zonectl -d /dev/${md}.zoned -c rz -P summary + atf_check_equal "4" \ + "$(zonectl -d /dev/${md}.zoned -c rz -P script | \ + awk 'END {print NR}')" +} +report_zones_cleanup() +{ + zoned_md_cleanup +} + +atf_test_case report_starting_lba cleanup +report_starting_lba_head() +{ + atf_set "descr" "zonectl -c rz -l reports zones from a starting LBA" + atf_set "require.user" "root" +} +report_starting_lba_body() +{ + alloc_zoned_md + atf_check gzoned create -s 256m ${md} + # Only zones 2 and 3 lie at or past LBA 0x100000 (512m). + atf_check_equal "2" \ + "$(zonectl -d /dev/${md}.zoned -c rz -l 0x100000 -P summary | \ + awk '/zones,/ {print $1}')" + atf_check_equal "0x100000" \ + "$(zonectl -d /dev/${md}.zoned -c rz -l 0x100000 -P script | \ + awk -F',' 'NR == 1 {gsub(/ /, "", $1); print $1}')" +} +report_starting_lba_cleanup() +{ + zoned_md_cleanup +} + +atf_test_case report_params cleanup +report_params_head() +{ + atf_set "descr" "zonectl -c params reports the device parameters" + atf_set "require.user" "root" +} +report_params_body() +{ + alloc_zoned_md + atf_check gzoned create -s 256m -m 3 ${md} + atf_check -o match:"Zone Mode: Host Managed" \ + -o match:"URSWRZ.*: Yes" \ + -o match:"Open Sequential Write Required Zones: 3" \ + zonectl -d /dev/${md}.zoned -c params +} +report_params_cleanup() +{ + zoned_md_cleanup +} + +atf_test_case open_zone cleanup +open_zone_head() +{ + atf_set "descr" "zonectl -c open explicitly opens a zone" + atf_set "require.user" "root" +} +open_zone_body() +{ + alloc_zoned_md + atf_check gzoned create -s 256m ${md} + atf_check zonectl -d /dev/${md}.zoned -c open -l 0 + atf_check_equal "1" "$(zone_count exp_open)" +} +open_zone_cleanup() +{ + zoned_md_cleanup +} + +atf_test_case close_zone cleanup +close_zone_head() +{ + atf_set "descr" "zonectl -c close closes an open zone" + atf_set "require.user" "root" +} +close_zone_body() +{ + alloc_zoned_md + atf_check gzoned create -s 256m ${md} + atf_check -e ignore dd if=/dev/zero of=/dev/${md}.zoned bs=1m count=1 + atf_check_equal "1" "$(zone_count imp_open)" + atf_check zonectl -d /dev/${md}.zoned -c close -l 0 + atf_check_equal "1" "$(zone_count closed)" + atf_check_equal "0" "$(zone_count imp_open)" +} +close_zone_cleanup() +{ + zoned_md_cleanup +} + +atf_test_case finish_zone cleanup +finish_zone_head() +{ + atf_set "descr" "zonectl -c finish transitions a zone to full" + atf_set "require.user" "root" +} +finish_zone_body() +{ + alloc_zoned_md + atf_check gzoned create -s 256m ${md} + atf_check -e ignore dd if=/dev/zero of=/dev/${md}.zoned bs=1m count=1 + atf_check zonectl -d /dev/${md}.zoned -c finish -l 0 + atf_check_equal "1" "$(zone_count full)" +} +finish_zone_cleanup() +{ + zoned_md_cleanup +} + +atf_test_case zone_state_handling cleanup +zone_state_handling_head() +{ + atf_set "descr" \ + "zone commands follow ZBC spec outside their transition states" + atf_set "require.user" "root" +} +zone_state_handling_body() +{ + alloc_zoned_md + atf_check gzoned create -s 256m -c 0 ${md} + + # Explicitly targeting a conventional zone is the caller's error. + atf_check -s not-exit:0 -e ignore \ + zonectl -d /dev/${md}.zoned -c finish -l 0 + # Closing an empty zone is a no-op. + atf_check zonectl -d /dev/${md}.zoned -c close -l 0x80000 + atf_check_equal "3" "$(zone_count empty)" + # So is opening a full zone: it must stay full. + atf_check zonectl -d /dev/${md}.zoned -c finish -l 0x100000 + atf_check zonectl -d /dev/${md}.zoned -c open -l 0x100000 + atf_check_equal "1" "$(zone_count full)" + atf_check_equal "0" "$(zone_count exp_open)" +} +zone_state_handling_cleanup() +{ + zoned_md_cleanup +} + +atf_init_test_cases() +{ + atf_add_test_case report_filter + atf_add_test_case report_zones + atf_add_test_case report_starting_lba + atf_add_test_case report_params + atf_add_test_case open_zone + atf_add_test_case close_zone + atf_add_test_case finish_zone + atf_add_test_case zone_state_handling +} diff --git a/usr.sbin/zonectl/zonectl.c b/usr.sbin/zonectl/zonectl.c index 462353cd58ef..631b3812e51d 100644 --- a/usr.sbin/zonectl/zonectl.c +++ b/usr.sbin/zonectl/zonectl.c @@ -292,13 +292,11 @@ zonectl_print_rz(struct disk_zone_report *report, zone_output_flags out_flags, (uintmax_t)entry->zone_start_lba, field_widths[ZONE_FW_LEN], (uintmax_t)entry->zone_length); - if (entry->write_pointer_lba == 0xffffffffffff || - entry->write_pointer_lba == 0xffffffffffffffff) { + if (DISK_ZONE_WP_INVALID(entry->zone_condition)) { /* - * This value is reported by HDDs for conventional - * zones. It really means "N/A". Reported it as -1, - * even though it's technically unsigned, to save - * space. + * The zone has no write pointer, which really means + * "N/A". Report it as -1, even though the field is + * technically unsigned, to save space. */ printf("%*d, ", field_widths[ZONE_FW_WP], -1); } else {