Skip to content

NAS-143717 / 27.0.0-BETA.1 / Convert the zfs.resource services to the typesafe pattern - #19698

Merged
Qubad786 merged 8 commits into
masterfrom
mrehan/typesafe-resource
Sep 15, 2026
Merged

Qubad786 merged 8 commits into
masterfrom
mrehan/typesafe-resource

Conversation

@Qubad786

Copy link
Copy Markdown
Contributor

Context

zfs.resource, zfs.resource.snapshot and zfs.resource.pool were already half-converted: public methods carried @api_method(check_annotations=True) against models in api/v27_0_0/, and most in-process consumers already went through call2. What was missing was the structural half - the service classes still held the implementation. resource_crud.py and snapshot_crud.py were 800+ lines each, mixing a 117-line create pipeline, destroy validation, path helpers and per-method exception translation in with the API surface.

Solution

Lean shims delegating to module functions. The three services keep only their Config, decorators and docstrings, and hand off to plain functions taking a ServiceContext - the same shape ports, hardware and boot already use. The logic lands in resource_{query,create,destroy,ops}.py, snapshot_ops.py and prefetch_ops.py. create_rules.py's three pool-inspecting helpers now take a ServiceContext rather than an untyped service, and while touching that file it picks up from __future__ import annotations so its annotations stop being quoted strings.

Each service sits in a module named after its namespace - resource.py, snapshot.py, prefetch.py, tier.py - and plugins/zfs/__init__.py stays a docstring. That is deliberate and worth keeping: around 30 modules elsewhere import cheap leaves of this package (exceptions, zvol_utils, utils, encryption), and test/integration/assets/pool.py - which 173 test files pull in, on the client side - is one of them. With the service in __init__.py, from middlewared.plugins.zfs.exceptions import ZFSPathNotFoundException went from a typing import to 1438 modules including middlewared.service and the truenas_pylibzfs C extension, which tests/requirements.txt does not install. It is now 145 and pulls neither. plugins/zpool/ already splits this way for the same reason.

zfs.resource.pool becomes a sub-service of zfs.resource instead of only being picked up by the plugin loader, which gives it a self.s path. That was the last thing forcing untyped string calls into this namespace - pool.dedup, the failover event handler and the pool.post_import hook all use call2 now, and no string call to zfs.resource* survives outside the over-the-wire tests.

@Qubad786
Qubad786 requested a review from a team September 14, 2026 04:00
@bugclerk bugclerk changed the title Convert the zfs.resource services to the typesafe pattern NAS-143717 / 27.0.0-BETA.1 / Convert the zfs.resource services to the typesafe pattern Sep 14, 2026
@bugclerk

Copy link
Copy Markdown
Contributor

Comment thread src/middlewared/middlewared/plugins/zfs/resource_create.py Outdated
Comment thread src/middlewared/middlewared/plugins/zfs/resource_create.py Outdated
Comment thread src/middlewared/middlewared/utils/zfs/tier.py Outdated
Comment thread src/middlewared/middlewared/plugins/zfs/snapshot_ops.py Outdated
Comment thread src/middlewared/middlewared/plugins/zfs/snapshot_ops.py
Comment thread src/middlewared/middlewared/plugins/zfs/snapshot.py Outdated
@Qubad786
Qubad786 requested a review from themylogin September 14, 2026 16:43

@themylogin themylogin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

Qubad786 and others added 8 commits September 15, 2026 07:23
## Context
`zfs.resource`, `zfs.resource.snapshot` and `zfs.resource.pool` were already half-converted: public methods carried `@api_method(check_annotations=True)` against models in `api/v27_0_0/`, and most in-process consumers already went through `call2`. What was missing was the structural half - the service classes still held the implementation. `resource_crud.py` and `snapshot_crud.py` were 800+ lines each, mixing a 117-line create pipeline, destroy validation, path helpers and per-method exception translation in with the API surface.

## Solution
**Lean shims delegating to module functions.** The three services keep only their `Config`, decorators and docstrings, and hand off to plain functions taking a `ServiceContext` - the same shape `ports`, `hardware` and `boot` already use. The logic lands in `resource_{query,create,destroy,ops}.py`, `snapshot_ops.py` and `prefetch_ops.py`. `create_rules.py`'s three pool-inspecting helpers now take a `ServiceContext` rather than an untyped `service`, and while touching that file it picks up `from __future__ import annotations` so its annotations stop being quoted strings.

**Each service sits in a module named after its namespace** - `resource.py`, `snapshot.py`, `prefetch.py`, `tier.py` - and `plugins/zfs/__init__.py` stays a docstring. That is deliberate and worth keeping: around 30 modules elsewhere import cheap leaves of this package (`exceptions`, `zvol_utils`, `utils`, `encryption`), and `test/integration/assets/pool.py` - which 173 test files pull in, on the client side - is one of them. With the service in `__init__.py`, `from middlewared.plugins.zfs.exceptions import ZFSPathNotFoundException` went from a `typing` import to 1438 modules including `middlewared.service` and the `truenas_pylibzfs` C extension, which `tests/requirements.txt` does not install. It is now 145 and pulls neither. `plugins/zpool/` already splits this way for the same reason.

**`zfs.resource.pool` becomes a sub-service** of `zfs.resource` instead of only being picked up by the plugin loader, which gives it a `self.s` path. That was the last thing forcing untyped string calls into this namespace - `pool.dedup`, the failover event handler and the `pool.post_import` hook all use `call2` now, and no string call to `zfs.resource*` survives outside the over-the-wire tests.

**`special_vdev_thresholds` moves to `utils/zfs/tier.py`.** It is pure arithmetic over two config fields and never belonged behind a plugin; `alert.source.zfs_tier` was importing it from `plugins.zfs.tier`, and alert sources are imported while `middlewared.service` is still initialising, so that pulled a plugin package in before `Service` was bound. Worth knowing that nothing static catches this class of breakage - mypy, lint, ruff and the whole unit suite were green on a tree that could not start.

`GenericCRUDService` is not an option here - its metaclass forces `filters`/`options` onto `query`, and `zfs.resource.query` takes a single request model. The `*_impl` methods also still return dicts, so nothing outside `plugins/zfs/` changes how it reads a result. Public wire shapes are identical, checked by replaying the full API surface on a test VM and diffing against the same run before the change.
@Qubad786
Qubad786 force-pushed the mrehan/typesafe-resource branch from c57e711 to a3b4ae4 Compare September 15, 2026 02:24
@Qubad786
Qubad786 merged commit b6ecda7 into master Sep 15, 2026
4 checks passed
@Qubad786
Qubad786 deleted the mrehan/typesafe-resource branch September 15, 2026 08:07
@bugclerk

Copy link
Copy Markdown
Contributor

This PR has been merged and conversations have been locked.
If you would like to discuss more about this issue please use our forums or raise a Jira ticket.

@truenas truenas locked as resolved and limited conversation to collaborators Sep 15, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants