FreeNAS/freenas cc82626src/middlewared/middlewared/api/base/types urls.py, src/middlewared/middlewared/api/base/validators base.py

Stop pydantic serializer warnings for HttpsOnlyURL fields

This commit fixes an issue where every serialization of an HttpsOnlyURL field logged a PydanticSerializationUnexpectedValue warning. The AfterValidator returned a str while the annotation stayed HttpUrl, so the serializer expected a Url but got a str. We now keep the value as an HttpUrl and attach a PlainSerializer(str) so model_dump still emits a plain string in both python and json modes without the warning.
DeltaFile
+6-2src/middlewared/middlewared/api/base/types/urls.py
+2-2src/middlewared/middlewared/api/base/validators/base.py
+8-42 files

FreeNAS/freenas a1bd83fsrc/middlewared/middlewared/plugins/mail_ outlook.py

Discard Outlook tokens that are about to expire, not ones that already have.
DeltaFile
+1-1src/middlewared/middlewared/plugins/mail_/outlook.py
+1-11 files

FreeNAS/freenas c6da844src/middlewared/middlewared/plugins mail.py

Fix mail.send omitting Cc recipients from the SMTP envelope
DeltaFile
+17-3src/middlewared/middlewared/plugins/mail.py
+17-31 files

FreeNAS/freenas 2a65180src/middlewared/middlewared/plugins/mail send.py send_queue.py, src/middlewared/middlewared/test/integration/assets mail.py

Mail plugin test coverage
DeltaFile
+447-25tests/api2/test_mail.py
+46-35src/middlewared/middlewared/plugins/mail/send.py
+60-4src/middlewared/middlewared/test/integration/fake_servers/smtp.py
+29-6src/middlewared/middlewared/plugins/mail/send_queue.py
+5-0src/middlewared/middlewared/test/integration/assets/mail.py
+3-0src/middlewared/middlewared/plugins/mail/queue.py
+590-704 files not shown
+593-7510 files

FreeNAS/freenas 6f8bfd5src/middlewared/middlewared/plugins/mail send.py send_queue.py, src/middlewared/middlewared/test/integration/assets mail.py

Mail plugin test coverage
DeltaFile
+447-25tests/api2/test_mail.py
+46-35src/middlewared/middlewared/plugins/mail/send.py
+60-4src/middlewared/middlewared/test/integration/fake_servers/smtp.py
+29-6src/middlewared/middlewared/plugins/mail/send_queue.py
+8-3src/middlewared/middlewared/plugins/mail/config.py
+5-0src/middlewared/middlewared/test/integration/assets/mail.py
+595-733 files not shown
+599-769 files

FreeNAS/freenas 9050b14src/middlewared/middlewared/plugins/mail send.py, src/middlewared/middlewared/test/integration/assets mail.py

Mail plugin test coverage
DeltaFile
+427-25tests/api2/test_mail.py
+60-4src/middlewared/middlewared/test/integration/fake_servers/smtp.py
+16-2src/middlewared/middlewared/plugins/mail/send.py
+5-0src/middlewared/middlewared/test/integration/assets/mail.py
+508-314 files

FreeNAS/freenas bf26d29src/middlewared/middlewared main.py, src/middlewared/middlewared/api/v27_0_0 common.py

Delete invalid cloud sync credentials and tasks from database
DeltaFile
+39-0src/middlewared/middlewared/plugins/datastore/write.py
+19-1src/middlewared/middlewared/service/crud_service_part.py
+5-0src/middlewared/middlewared/plugins/cloud_credentials/__init__.py
+5-0src/middlewared/middlewared/api/v27_0_0/common.py
+4-0src/middlewared/middlewared/main.py
+2-0src/middlewared/middlewared/plugins/cloud_sync/__init__.py
+74-11 files not shown
+75-17 files

FreeNAS/freenas 6804783src/middlewared/middlewared/api/v26_0_0 container.py, src/middlewared/middlewared/migration 0020_repair_incus_clone_origins.py

Relocate migrated container origins out of legacy .ix-virt

## Problem
Incus containers are ZFS clones of an image snapshot. The incus->container auto-migration relocated each container from `<pool>/.ix-virt/containers/<name>` to `<pool>/.truenas_containers/containers/<name>` with a bare `zfs rename` and did nothing else. A `zfs rename` does not change a clone's `origin`, so a migrated container stayed a clone of a snapshot still living inside `.ix-virt`. `.ix-virt` was visible in the UI and not delete-guarded, so deleting it recursively destroyed those origin snapshots and cascaded into the dependent migrated clones — silently destroying migrated containers.

## Solution
Relocate each container's origin image dataset out of `.ix-virt` before renaming the container, so no migrated container depends on anything under `.ix-virt`.

- **Shared relocation helper** — `container.relocate_container_origin` reads a container's live `origin`; if it points at an image under `.ix-virt/images` or `.ix-virt/deleted/images`, it renames that image dataset into the native `.truenas_containers/images/` tree, tags it `truenas:origin=incus-migration`, and sets `canmount=noauto`. All fan-out clones auto-repoint on the rename; an origin already outside `.ix-virt` is left alone; a relocation failure leaves the container wholly inside `.ix-virt` (best-effort, skip).
- **Migration path** — the incus->container migration calls the helper immediately before renaming each container, and skips any container whose base image cannot be relocated.
- **Repair migration** — new `0020_repair_incus_clone_origins` runs the same relocation over existing `container.container` rows for systems that already ran the old migration.
- **Delete guard** — `.truenas_containers` is added to `INTERNAL_PATHS` so it cannot be deleted out from under running containers; the plugin's own snapshot/clone/destroy calls that touch it now pass `bypass=True`.
- **Image garbage collection** — on container delete, a relocated origin image is destroyed once its last clone is gone, keyed on the `truenas:origin` tag so native image-cache datasets are never reaped. The image dataset is destroyed recursively so any snapshots it later accumulated (e.g. from a periodic snapshot task) do not block reclaim; this is safe because it only runs once the origin snapshot is confirmed clone-free.
- **Safer deletion** — `do_delete` destroys the dataset first (recursively, matching the apps stack, so a container that has snapshots is deletable) and removes the database and libvirt records only once the dataset is confirmed gone, so a failed destroy never orphans the dataset with no row pointing at it; an already-missing dataset is tolerated so a container whose data was lost to the old cascade can still be removed cleanly. Delete is now a single-locked job so concurrent deletes of fan-out siblings cannot race each other's image garbage collection.
- **Active-instance guards** — deleting or renaming a container that is not stopped (running or suspended) is refused; delete additionally accepts `force=True`, which stops it first, mirroring the VM delete flow. The container status model now includes the `SUSPENDED` state it can actually report.
DeltaFile
+124-8src/middlewared/middlewared/plugins/container/container.py
+105-0src/middlewared/middlewared/plugins/container/migrate.py
+41-0src/middlewared/middlewared/migration/0020_repair_incus_clone_origins.py
+11-1src/middlewared/middlewared/api/v26_0_0/container.py
+4-3src/middlewared/middlewared/plugins/container/image.py
+2-1src/middlewared/middlewared/plugins/zfs/utils.py
+287-131 files not shown
+288-147 files

FreeNAS/freenas a3c7876src/middlewared/middlewared/plugins/container lifecycle.py crud.py, src/middlewared/middlewared/plugins/vm lifecycle.py crud.py

NAS-141844 / 27.0.0-BETA.1 / Accept a pydantic model or dict in pylibvirt_vm/pylibvirt_container (#19348)

## Context
Every lifecycle and delete caller had to hand-dump its model before
calling these helpers, and for VMs that dump had to pass
`expose_secrets=True` or the display device's `Secret` password would be
silently redacted and a broken domain shipped. Leaking that invariant to
each call site was repetitive and easy to get wrong.

## Solution
`pylibvirt_vm` and `pylibvirt_container` now accept either a pydantic
model or a dict and do the `model_dump` internally — VMs with
`expose_secrets=True`, containers with a plain dump. Callers pass the
model directly, so the secret-exposure rule lives in one place. The
state-gathering factories in `extend_context_sync` keep passing their
raw pre-extend rows through the dict branch, since no full model exists
yet at that point.
DeltaFile
+28-27src/middlewared/middlewared/plugins/container/lifecycle.py
+22-18src/middlewared/middlewared/plugins/vm/lifecycle.py
+28-5src/middlewared/middlewared/plugins/vm/crud.py
+26-6src/middlewared/middlewared/plugins/container/crud.py
+104-564 files

FreeNAS/freenas 0644b83src/middlewared/middlewared/alembic/versions/27.0 2026-07-21_08-51_s3_null_region.py

Fix S3 NULL region in database
DeltaFile
+45-0src/middlewared/middlewared/alembic/versions/27.0/2026-07-21_08-51_s3_null_region.py
+45-01 files

FreeNAS/freenas 87fdeac.github/workflows lint.yml, src/middlewared/middlewared/api/base model.py

NAS-141794 / 27.0.0-BETA.1 / Wrap all `Secret` defaults in `Secret` (#19322)

Otherwise, explicitly set values and default values will be handled
differently
DeltaFile
+49-0src/middlewared/middlewared/pytest/unit/api/base/test_secret_default_wrap.py
+21-2src/middlewared/middlewared/api/base/model.py
+1-1.github/workflows/lint.yml
+71-33 files

FreeNAS/freenas bc1d671tests/api2 test_replication.py

Parametrize attachment-delegate toggle test over replication and snapshot tasks
DeltaFile
+39-21tests/api2/test_replication.py
+39-211 files

FreeNAS/freenas 5adf815src/middlewared/middlewared/plugins/pool_ import_pool.py export.py

NAS-141848 / 26.0.0-BETA.3 / Handle type-safe snapshot-task attachments in pool export/import (by creatorcary) (#19350)

Non-cascade `pool.export` crashed with
`'PeriodicSnapshotTaskQueryResultItem' object is not subscriptable`,
aborting the export after it had already disabled the pool's
shares/services and its snapshot tasks.

The export/import attachment loops iterate every delegate's `query()`
results and subscript them as dicts (`attachment['id']`).
[NAS-139294](https://ixsystems.atlassian.net/browse/NAS-139294) made
`pool.snapshottask` return type-safe Pydantic models, so the
snapshot-task delegate now yields model objects that aren't
subscriptable. Guard both call sites (`pool_/export.py` and
`pool_/import_pool.py`) to fall back to attribute access, matching the
fix already on master in
[#19278](https://github.com/truenas/middleware/pull/19278).


Original PR: https://github.com/truenas/middleware/pull/19349

Co-authored-by: Logan Cary <logan.cary at ixsystems.com>
DeltaFile
+4-1src/middlewared/middlewared/plugins/pool_/import_pool.py
+4-1src/middlewared/middlewared/plugins/pool_/export.py
+8-22 files

FreeNAS/freenas be2ae3fsrc/middlewared/middlewared/plugins/pool_ export.py import_pool.py

Handle type-safe snapshot-task attachments in pool export/import

(cherry picked from commit 1cca4de27e56691dbc09dea662ce5f957248d821)
DeltaFile
+4-1src/middlewared/middlewared/plugins/pool_/export.py
+4-1src/middlewared/middlewared/plugins/pool_/import_pool.py
+8-22 files

FreeNAS/freenas 17ec580src/middlewared/middlewared/plugins/pool_ export.py import_pool.py

NAS-141848 / 26.0.0-RC.1 / Handle type-safe snapshot-task attachments in pool export/import (#19349)

Non-cascade `pool.export` crashed with
`'PeriodicSnapshotTaskQueryResultItem' object is not subscriptable`,
aborting the export after it had already disabled the pool's
shares/services and its snapshot tasks.

The export/import attachment loops iterate every delegate's `query()`
results and subscript them as dicts (`attachment['id']`).
[NAS-139294](https://ixsystems.atlassian.net/browse/NAS-139294) made
`pool.snapshottask` return type-safe Pydantic models, so the
snapshot-task delegate now yields model objects that aren't
subscriptable. Guard both call sites (`pool_/export.py` and
`pool_/import_pool.py`) to fall back to attribute access, matching the
fix already on master in
[#19278](https://github.com/truenas/middleware/pull/19278).
DeltaFile
+4-1src/middlewared/middlewared/plugins/pool_/export.py
+4-1src/middlewared/middlewared/plugins/pool_/import_pool.py
+8-22 files

FreeNAS/freenas 1cca4desrc/middlewared/middlewared/plugins/pool_ export.py import_pool.py

Handle type-safe snapshot-task attachments in pool export/import
DeltaFile
+4-1src/middlewared/middlewared/plugins/pool_/export.py
+4-1src/middlewared/middlewared/plugins/pool_/import_pool.py
+8-22 files

FreeNAS/freenas 0fcd519src/middlewared/middlewared/plugins/cloud_sync __init__.py

Fix circular import
DeltaFile
+1-1src/middlewared/middlewared/plugins/cloud_sync/__init__.py
+1-11 files

FreeNAS/freenas 705563atests/cloud test_cloud_backup.py test_sftp.py

Cloud sync tests coverage
DeltaFile
+155-0tests/cloud/test_cloud_backup.py
+148-0tests/cloud/test_sftp.py
+78-0tests/cloud/test_cloud_sync_crud_validation.py
+78-0tests/cloud/test_cloud_sync_path.py
+459-04 files

FreeNAS/freenas c103a41tests/cloud test_cloud_sync.py test_cloud_sync_validation.py

Near-full cloud sync plugin test coverage
DeltaFile
+169-0tests/cloud/test_cloud_sync.py
+137-0tests/cloud/test_cloud_sync_validation.py
+19-0tests/cloud/test_cloud_sync_crud.py
+325-03 files

FreeNAS/freenas 5c7c8a1src/middlewared/middlewared/api/base model.py

Use `DumpableModel` as base for `NormalizedQuestions`
DeltaFile
+0-42src/middlewared/middlewared/api/base/model.py
+0-421 files

FreeNAS/freenas 9e26c0esrc/middlewared/middlewared main.py, src/middlewared/middlewared/plugins/cloud_credentials verify.py

`ruff`
DeltaFile
+0-6src/middlewared/middlewared/main.py
+0-5src/middlewared/middlewared/plugins/cloud_credentials/verify.py
+0-112 files

FreeNAS/freenas b062f4asrc/middlewared/middlewared/plugins/cloud crud.py script.py

`plugins/cloud` mypy
DeltaFile
+23-10src/middlewared/middlewared/plugins/cloud/crud.py
+11-3src/middlewared/middlewared/plugins/cloud/script.py
+7-1src/middlewared/middlewared/plugins/cloud/snapshot.py
+7-1src/middlewared/middlewared/plugins/cloud/remotes.py
+1-1src/middlewared/middlewared/plugins/cloud/model.py
+1-1src/middlewared/middlewared/plugins/cloud/path.py
+50-172 files not shown
+52-188 files

FreeNAS/freenas 85ce97a.github/workflows lint.yml, src/middlewared/middlewared/api/base model.py

Wrap all `Secret` defaults in `Secret`
DeltaFile
+49-0src/middlewared/middlewared/pytest/unit/api/base/test_secret_default_wrap.py
+21-2src/middlewared/middlewared/api/base/model.py
+1-1.github/workflows/lint.yml
+71-33 files

FreeNAS/freenas 20e5262.github/workflows ruff.yml, src/middlewared pyproject.toml

`ruff`
DeltaFile
+6-1src/middlewared/pyproject.toml
+1-1.github/workflows/ruff.yml
+7-22 files

FreeNAS/freenas 30ffb6csrc/middlewared/middlewared/plugins/cloud_sync rclone.py crud.py, tests/cloud test_sftp.py test_cloud_sync_path.py

`ruff`
DeltaFile
+112-52src/middlewared/middlewared/plugins/cloud_sync/rclone.py
+71-55tests/cloud/test_sftp.py
+23-13src/middlewared/middlewared/plugins/cloud_sync/crud.py
+20-16tests/cloud/test_cloud_sync_path.py
+21-13tests/cloud/test_cloud_sync_validation.py
+19-14tests/cloud/test_cloud_sync_crud_validation.py
+266-16313 files not shown
+364-21719 files

FreeNAS/freenas b705269src/middlewared/middlewared/plugins cloud_sync.py, src/middlewared/middlewared/plugins/cloud crud.py

WIP
DeltaFile
+0-1,036src/middlewared/middlewared/plugins/cloud_sync.py
+507-0src/middlewared/middlewared/plugins/cloud_sync/rclone.py
+215-0src/middlewared/middlewared/plugins/cloud_sync/__init__.py
+144-48src/middlewared/middlewared/plugins/cloud/crud.py
+177-0src/middlewared/middlewared/plugins/cloud_sync/crud.py
+136-0src/middlewared/middlewared/plugins/cloud_sync/sync.py
+1,179-1,08468 files not shown
+2,305-1,88174 files

FreeNAS/freenas 35bcbcbsrc/middlewared/middlewared/api/base model.py

Protect from `expose_secrets` ambiguity
DeltaFile
+5-0src/middlewared/middlewared/api/base/model.py
+5-01 files

FreeNAS/freenas b75255dsrc/middlewared/middlewared/utils plugins.py

Deterministic `load_modules` order
DeltaFile
+2-2src/middlewared/middlewared/utils/plugins.py
+2-21 files

FreeNAS/freenas 762fa9dsrc/middlewared/middlewared/api/base model.py

Change `model_dump` to have `by_alias=True` and `warnings=False` by default
DeltaFile
+37-0src/middlewared/middlewared/api/base/model.py
+37-01 files

FreeNAS/freenas c7f2fc9src/middlewared/middlewared/plugins/directoryservices_ ipa_join_mixin.py secrets.py, src/middlewared/middlewared/utils/directoryservices ipa_constants.py

Fix IPA SMB machine account setup and self-heal old joins

Set the SMB machine-account password from the generated value when
retrieving the keytab and write that same value to secrets.tdb as raw
bytes, so the keytab and secrets.tdb agree. Report changesecretpw
stderr on failure and drop the unawaited backup() call.

Regenerate smb.conf before creating the SMB service principal so the
machine account uses the current NetBIOS name, and make principal
creation failures fatal so partial joins roll back.

Stamp a credential version in secrets.tdb; the IPA health check
regenerates the SMB machine account in place, via the host credential,
for systems joined by an earlier build that wrote it wrong. Expose the
same regeneration as an explicit recovery action.

Add unit and integration tests.
DeltaFile
+110-42src/middlewared/middlewared/plugins/directoryservices_/ipa_join_mixin.py
+69-0tests/unit/test_directoryservices_secrets.py
+47-0tests/directory_services/test_ipa_join.py
+35-5src/middlewared/middlewared/plugins/directoryservices_/secrets.py
+18-0src/middlewared/middlewared/plugins/directoryservices_/ipa_health_mixin.py
+7-0src/middlewared/middlewared/utils/directoryservices/ipa_constants.py
+286-472 files not shown
+288-488 files