Skip to content

fix(proxmox): make host registration idempotent on name - #137

Open
pparage wants to merge 2 commits into
devfrom
fix/proxmox-host-seed-idempotent
Open

fix(proxmox): make host registration idempotent on name#137
pparage wants to merge 2 commits into
devfrom
fix/proxmox-host-seed-idempotent

Conversation

@pparage

@pparage pparage commented Aug 10, 2026

Copy link
Copy Markdown
Member

The bug

The deploy bundle POSTs /v1/proxmox/hosts on every scenario run, and nothing stopped a second row under an existing name — create_host never checked for a clash and proxmox_hosts.name had no constraint. The documented "idempotent via 409" was unreachable: 409 could never be returned. Each re-run added a host, and the UI picker filled up with duplicates of the same hypervisor.

Why upsert rather than 409

Delete-and-recreate is not a viable re-seed path: deployments.target_host_id is a FK to proxmox_hosts.id, so a new row per re-run strands every earlier deployment on a host nobody updates.

So POST now upserts on name and keeps the id, returning 200 instead of 201. added_at is deliberately untouched — it records first registration, and the migration keys on it.

Credentials are part of what gets refreshed, which closes a second trap: there is no PUT/PATCH on hosts and the UI is read-only on them, so a rotated PVE token was previously unfixable short of a manual DB edit. It now reaches the backend on the next deploy.

Migration 0002

Databases in the field already carry duplicates, and the constraint cannot be added on top of them. For each name the migration keeps the earliest added_at row, repoints any deployments at it, deletes the rest, and logs every collapse:

proxmox_hosts: collapsed duplicate 'pve01' (d4e5f6) into a1b2c3, repointed 2 deployment(s)

Verified

  • 6 tests, written RED-first. Route tests failed for the right reasons (two token rows in the DB, second POST returning 201); migration tests on DID NOT RAISE IntegrityError and un-repointed FKs.
  • The migration is tested through alembic upgrade, not by inspection: a 0001-era DB seeded with three pve01 duplicates plus deployments pointing at the later two, then asserted on the survivor and the repointing. Plus a no-op case on clean data.
  • 480 passed, ruff clean, openapi.json regenerated for the drift gate.

Pairs with

range42/range42-playbooks#142, which drops 409 from the bundle's accepted status codes — keeping it would only mask a real conflict.

The deploy bundle POSTs /v1/proxmox/hosts on every scenario run, and nothing
stopped a second row under an existing name: create_host never checked for a
clash and proxmox_hosts.name had no constraint, so the documented "idempotent
via 409" was never reachable. Each re-run added a host, and the UI picker
filled up with duplicates of the same hypervisor.

Delete-and-recreate was not an option for the fix: deployments.target_host_id
is a FK to proxmox_hosts.id, so a new row per re-run strands every earlier
deployment on a host nobody updates. POST now upserts on name and keeps the
id, returning 200 instead of 201. Credentials are refreshed too, so a rotated
PVE token reaches the backend on the next deploy instead of leaving it
authenticating with a stale one -- there is no update route to fix it by hand.

Migration 0002 collapses duplicates already in the field before adding the
constraint: for each name it keeps the earliest added_at row, repoints any
deployments at it, deletes the rest, and logs every collapse.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 83d279487c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +36 to +38
rows = conn.execute(
sa.text("SELECT id, name FROM proxmox_hosts ORDER BY name, added_at, id")
).fetchall()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve the newest host configuration during deduplication

When duplicate registrations contain a rotated token or updated URL/node, sorting oldest-first and retaining that entire row permanently deletes the newest configuration while repointing every deployment to the stale credentials. This can make existing deployments and health checks fail immediately after upgrade, before another deploy bundle happens to re-register the host. Keep the oldest ID if identity preservation is required, but copy the newest duplicate's mutable host fields onto it before deleting the later rows.

Useful? React with 👍 / 👎.

Comment thread app/routes/v1/proxmox/hosts.py Outdated
Comment on lines +100 to +104
existing = (
await session.execute(
select(ProxmoxHost).where(ProxmoxHost.name == payload.name)
)
).scalar_one_or_none()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Make the name-based upsert atomic

When two scenario runs register the same previously unseen name concurrently, both requests can complete this query before either inserts; each then takes the create path, and the second commit violates uq_proxmox_host_name, producing an unhandled database error instead of the promised idempotent 200 response. Use an atomic database upsert or recover from the unique-conflict by rolling back, loading the winning row, and applying the update.

Useful? React with 👍 / 👎.

…om a lost race

Two problems found in review of the previous commit.

The migration kept the oldest duplicate wholesale. Duplicates accumulated one
per scenario re-run, so the newest row holds the credentials in force -- and
keeping the first row resurrected a token that may have been rotated away,
with every deployment repointed at it. The keeper now takes the newest
duplicate's api_url / node / token / bridge / overrides while keeping its own
id and added_at. The earlier test asserted on ids only, which is why it did
not catch this.

The upsert's duplicate check and its commit are separated by an await, so two
concurrent registrations of the same new name could both take the insert path
-- WEB_CONCURRENCY=1 bounds this to one process but not to one task. The
loser now rolls back, re-reads the winner and applies its update, matching
how create_deployment already handles the same shape.
@hyde-repo hyde-repo assigned hyde-repo and pparage and unassigned hyde-repo Aug 10, 2026
@hyde-repo
hyde-repo self-requested a review August 10, 2026 11:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants