Internal role: always active — no enable flag, no play tag, no induced-failure slug. It runs under the always tag, so it executes even on partial (-t <test>) runs.
Nothing — this is the suite's cleanup engine, not a test. It guarantees the "always clean" contract: every resource a validation run created (VMs, VM groups, vnets, flow services and templates, exported images/templates, files, packages, processes, bridge IPs) is deleted at the end of the run even when tests fail mid-way, resources that pre-existed (reused) are never touched, and anything it could not delete is reported as a red LEFTOVER row in the report's Execution & Cleanup Summary instead of silently surviving. It also precleans the previous run's leftovers before this run starts, so a stale broken resource (e.g. an err-state image) cannot poison this run's own resource reuse.
The role has two wiring points in playbooks/validation.yml, both tagged always:
- Pre-run preclean (
tasks_from: preclean.yml, primary frontend only, right after the firstvalidation_commonplay and before any test role): loads the per-host manifest mirror/tmp/odv_manifest.jsonleft by the previous run (either its leftovers-only rewrite, or a full manifest from an aborted run). Cluster-object entries (vm,service,service_template,vnet,template,image) are deleted best-effort right away; host-local entries (file,package,process,ip_addr),host_stateandvm_groupare folded unchanged into this run'sodv_manifestfact so the end-of-run sweep handles them. Images get an extraoneimage delete --forcefallback (7.4 CLI) after the normal delete fails to converge — exactly the stuck err-image case that motivates the preclean. A uniform presence re-check then rewrites the mirror to what is still unresolved (kept in the file only, deliberately not in the fact — a stalecreateddisposition from a prior run must not collide with a resource this run legitimately reuses under the same name). - Final sweep (the role itself, last play before the report, on frontends + nodes, no
run_*gate; no-ops on an empty manifest): each host re-loads and merges its mirror (so a cleanup-only re-run after an abort still works — a corrupt mirror degrades to a warning), all hosts'odv_manifestfacts are aggregated, andtasks/lib/plan.ymlturns the aggregate into a sweep plan. OpenNebula objects are deleted on the primary frontend only (sweep_one_objects.yml), host-local types on every host (sweep_local.yml), and every plan entry is recorded as anodv_sweep_resultsrow. Finally the mirror is rewritten to hold only this run's unresolved leftovers, so the next run's preclean retries them — and cannot resurrect already-deletedcreatedentries against a same-named resource the operator re-created in between.
Plan rules (tasks/lib/plan.yml, pure logic, unit-tested by test/harness/test_logic.yml):
- Entries are deduped by
(type, name)— by(type, name, host)for the host-local typesfile/package/process/ip_addr. - If any duplicate says
disposition: created, the resource is ours →delete(created wins over reused); all-reused→keep-reused. image/templateentries that would be deleted becomekeep-by-flagwhenvalidation.cleanup.keep_exported_imagesis set.host_stateentries are excluded entirely — restoring host state is the owning role's own job; that role reports failures viaodv_leftovers.- The first non-empty
idamong the duplicates wins.
Deletion style (both sweep files and the preclean): every delete runs as oneadmin with failed_when: false and prints an ABSENT/GONE/STUCK sentinel; already-absent resources are a clean no-op. In the final sweep (sweep_one_objects.yml), vm/template/image objects are resolved by name first; the manifest id is only a fallback and is used only if the object's current name still matches the manifest entry — a stale mirror or poisoned id can never kill an unrelated bystander. VMs get onevm terminate --hard with onevm recover --delete fallback and a converge wait; services oneflow delete with oneflow recover --delete fallback. A second net then catches anything that escaped the manifest, by the suite's deterministic name conventions: VMs matching ^(odv-|conn-mtx-vm-on-host|test_vm_ha|gpu-bench-) and stray flow-smoke-svc services/service templates (gpu-bench- is skipped when gpu_benchmark.keep_stack_on_failure is set).
Local types (sweep_local.yml, every host, this host's plan entries only): file → file: state=absent; ip_addr (name contract <ip>/32:dev:<bridge>) → ip addr del + ip route del; process (name contract <binary>:<port>) → pkill -f with the pattern's first character bracketed so the wrapper shell never matches itself (pkill rc 0 and 1 both count as success); package → package: state=absent. A failed removal becomes a leftover row, the play continues.
- OpenNebula CLI on the primary frontend, working as
oneadminvia become:onevm,onevnet,onetemplate,oneimage,onevmgroup,oneflow,oneflow-template, plusjq(used to verify names on id-fallback deletes). The image--forcefallback needs the OpenNebula 7.4 CLI. - Write access to
/tmp/odv_manifest.jsonon every host (the per-host manifest mirror). - On hypervisor hosts:
ip,pkilland the OS package manager, for the host-local sweeps.
| Option | Default | Description |
|---|---|---|
validation.cleanup.keep_exported_images |
false (reference inventory: false) |
Keep image/template resources the run created (keep-by-flag) instead of deleting them. Useful for iterative runs — the vLLM appliance image is multi-GB. |
gpu_benchmark.keep_stack_on_failure |
false |
Top-level key read by the sweep's second net only: when set, stray gpu-bench-* VMs are excluded from the name-pattern kill so a failed GPU stack survives for post-mortem. Distinct from gpu_benchmark.k8s.keep_stack_on_failure, which is what the gpu-benchmark role's own teardown honors — set both to keep the whole stack. |
frontend_group |
frontend |
Inventory group whose first host runs the preclean and the OpenNebula-object sweep (global suite variable, not under validation.). |
node_group |
node |
Inventory group swept (together with the frontends) for host-local resources (global suite variable). |
No odv_fail_in slug exists for this role — the debug fail hooks live in the test roles.
- Wiring: last play before the report,
hosts: frontends + nodes,tags: [always], no gate (skipped only in the GPU-benchmarkprephase, like the rest of the suite). - Inputs: every play host's
odv_manifestfact (rows appended byvalidation_common/manifest_add.yml:{type, id, name, cluster, role, disposition, host}) merged with that host's/tmp/odv_manifest.jsonmirror;validation.cleanup.keep_exported_images;gpu_benchmark.keep_stack_on_failure. - Registers: per-host
odv_sweep_resultsrows — the plan entry plusaction(removed|kept (reused)|kept (by flag)|leftover) anddetail(the delete's output, truncated to 200 chars, onleftover; second-net rows carry rolesweep (second net)— their removed VM rows and all stray-flow rows carry detailfound by name pattern, not in manifest, leftover VM rowsterminate did not converge). Kept rows and OpenNebula-object rows are recorded on the primary frontend; local-type rows on their own host. - Side effect: rewrites
/tmp/odv_manifest.jsonto this host'sleftoverrows only.
- Wiring: own play right after the first (
validation_common) play, primary frontend only,tags: [always]. - Inputs:
/tmp/odv_manifest.jsononly (an aborted previous run leaves no facts). Missing/corrupt mirror → clean no-op. - Registers: folds carry-through (
file/package/process/ip_addr/host_state/vm_group) mirror entries into this run'sodv_manifestfact; records theverification_resultkeyPre-run leftover cleanup=removed N leftover(s) from a previous runviavalidation_common/record_result.yml— only when N > 0, so a clean previous run adds no report row; VM deletion goes throughvalidation_common/terminate_vm_by_name.yml(tvbn_role: "sweep (pre-run)"), which appendsodv_leftoversrows for any VM it could not terminate. - Side effect: rewrites the mirror to carry-through entries + still-present cluster objects.
- Inputs: fact
_sweep_manifest(aggregated manifest entries),validation.cleanup.keep_exported_images. - Registers: fact
_sweep_plan— deduped entries withaction: delete | keep-reused | keep-by-flagand the best-knownid,host_stateexcluded. No side effects; this is the piece the logic harness unit-tests.
tasks/sweep_one_objects.yml and tasks/sweep_local.yml are internal to main.yml (they consume _sweep_plan) and are not meant to be included from elsewhere.
The report play on localhost aggregates, from all hosts: odv_manifest → report_manifest, odv_sweep_results → report_sweep_results, odv_leftovers → report_leftovers. The Execution & Cleanup Summary section renders unconditionally: a count line (with an explicit warning when the manifest is non-empty but the sweep recorded nothing — i.e. the sweep play did not run), then one table row per sweep result. removed / kept (reused) / kept (by flag) render as plain text; action: leftover rows and every odv_leftovers row render as red LEFTOVER with the failure detail (and host, for odv_leftovers). The preclean's Pre-run leftover cleanup row renders as INFO in the main results table.
This role is the cleanup; it creates nothing and never aborts the play — every delete is tolerant (failed_when: false / ignore_errors), and a corrupt manifest mirror degrades to a warning. Its failure mode is the LEFTOVER row: a resource that survived both its owning role's own always:/rescue: cleanup and this sweep (and, on the next run, the preclean's retry — including the image --force fallback) needs an operator to look at it. The leftovers-only mirror rewrite is what makes retries safe across runs: leftovers persist on purpose, resolved created entries must not.