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.
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.
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.
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
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>
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).
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.