feat: uncovered devices can be cleaned up, not just counted - #373
Merged
Conversation
A customer reported 552 uncovered devices and a count that keeps growing. The previous change answered why each one is uncovered and whether the number is rising; nothing made any of them removable. The prune looks like it should have. It does not. It acts on devices claimed by the run that produced the current result, and a device created by an earlier run and dropped from scope since is never in that set. This customer's shape is already a test case - orphans zero, owned-uncovered present - which is the prune being a no-op for them while the count climbs. The bucket was diagnosable and not actionable, and that gap is structural rather than a setting they missed. Three changes. Absence streaks now cover the uncovered set. They were kept for orphans alone, and the quarantine partition fails closed, so an uncovered device had no absence row and could never leave quarantine however long it had been gone. Without this a cleanup is a permanent no-op that looks like it works. The orphan prune's delete loop - ownership release, PROTECT handling, child-before-parent ordering, cyclic-claim fail-closed - moves into one helper both callers use. A second copy of it is how one path ends up without a guard the other has, and these deletes are permanent. The cleanup itself is gated on cause. Only devices the census marks absent are eligible: one Forward still reports without an include tag is a scoping decision, and one the vendor guard excluded is that guard working. A census that did not run raises rather than reading as "nothing is absent", because those two are identical in the counts and only one of them makes deleting safe. The shrink guard is new rather than reused. The existing one measures orphans against what the run previously claimed and returns early when orphans are zero, which is exactly the shape this exists for - it would have guarded nothing. This one measures the set being deleted against every device the sync has created, with the same absolute floor before the ratio. Disabling a device in Forward removes it from the API exactly as decommissioning does, confirmed against this customer's live snapshot from both the NQE result and the REST inventory. Nothing available to this plugin tells them apart, so the quarantine is what makes this safe rather than merely gated: a maintenance window does not outlast it. Exposed as `--prune-uncovered` on the audit command, dry run unless `--apply` is passed. Not a button: the orphan prune's button is what caused the harm this repo has on record, and reading dry-run JSON is a different act from clicking red. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KT2R17AeokBdgaC3FmVR75
The release was cut but never authorised or tagged, so what it contains is still open. The customer who reported the growing uncovered count reported it against this lane, and holding the fix for 2.9.4 would make them wait a release for something 2.9.3 can carry. The three compatibility tables and the changelog say so now. The evidence recorded on 2026-09-05 describes a tree without this and is superseded; the gate has to run again before the tag, which it would have anyway. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KT2R17AeokBdgaC3FmVR75
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Devices this sync created that Forward no longer reports can now be deleted,
not only counted. Exposed as
--prune-uncoveredonforward_device_scope_reconciliation_audit, dry run unless--apply.Folded into 2.9.3, which is cut but not tagged. The three compatibility tables
and the changelog say so.
Why the prune did not already do this
prune_orphan_devicesacts onout_of_scope = (previously_managed & netbox_names) - tagged_names- devicesclaimed by the run that produced the current result. A device created by an
earlier run and dropped from scope since is never in that set.
A customer's shape is already a test case in
test_uncovered_absence_and_trend.py: orphans zero, owned-uncovered present.For them the prune button is a no-op while the uncovered count climbs. The
bucket was diagnosable and not actionable, and the gap is structural rather
than a setting anyone missed.
Three changes
Absence streaks cover the uncovered set. They were kept for orphans only,
and
partition_quarantined_orphansfails closed, so an uncovered device had noabsence row and could never leave quarantine however long it had been gone.
Without this the cleanup is a permanent no-op that looks like it works.
One delete path. The orphan prune's loop - ownership release, PROTECT
handling, child-before-parent ordering, cyclic-claim fail-closed - moves into
_delete_prunable_devicesand both callers use it. A second copy is how onepath acquires a guard the other lacks, and these deletes are permanent.
Gated on cause. Only devices the census marks
absentare eligible. OneForward still reports without an include tag is a scoping decision;
vendor_excludedis the vendor guard working. A census that did not run raisesrather than reading as "nothing is absent" - those are identical in the counts
and only one makes deleting safe.
The shrink guard is new rather than reused: the orphan version measures orphans
against what the run previously claimed and returns early when orphans are
zero, which is exactly the shape this exists for. This one measures the set
being deleted against every device the sync created, with the same absolute
floor before the ratio.
Safety
Disabling a device in Forward removes it from the API exactly as
decommissioning does - confirmed against a live snapshot, from both the NQE
result and the REST inventory. Nothing available to this plugin tells them
apart, so the quarantine is what makes this safe rather than merely gated: a
maintenance window does not outlast it.
Not a UI button. The orphan prune's button caused the harm this repo has on
record; reading dry-run JSON is a different act from clicking red.
Tests
test_uncovered_device_cleanup.py, 15 tests, mostly negative space: a deviceForward still reports is never deleted; a vendor-excluded one is never deleted;
a device this sync did not create is never deleted; a recent absence is held;
no absence row at all is held; a failed census refuses; an empty Forward result
refuses; deleting most of what the sync created refuses; both overrides work.
Full
invoke cigreen on this tree: 412 harness, 70 scenario, 2643 Django(9 skipped), bulk-merge scale, org-query audit, playwright, docs, package, and
the artifact upgrade gate.
🤖 Generated with Claude Code
https://claude.ai/code/session_01KT2R17AeokBdgaC3FmVR75