fix(host_agent): deploy/provision 500 — double-begin on session
fetch_scope_targets() runs a SELECT that autobegins the session transaction (SQLAlchemy 2.0), so the following `async with db.begin()` raised "A transaction is already begun on this Session" → 500 on both /plugins/host_agent/deploy and /provision. Mint registrations directly into the autobegun transaction, build the inventory while the ORM objects are still live, then await db.commit(). Escaped CI because unit tests use a mock db_sessionmaker; no integration test exercises these routes (follow-up noted in Scribe issue #884). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -731,17 +731,19 @@ async def deploy_via_ansible():
|
|||||||
targets = await fetch_scope_targets(db, scope)
|
targets = await fetch_scope_targets(db, scope)
|
||||||
if not targets:
|
if not targets:
|
||||||
return _error(400, "no_targets", "No Ansible targets in that scope")
|
return _error(400, "no_targets", "No Ansible targets in that scope")
|
||||||
|
# The read above autobegins the session transaction (SQLAlchemy 2.0), so
|
||||||
|
# mint into it directly and commit — a nested db.begin() would double-begin.
|
||||||
tokens: dict[str, str] = {}
|
tokens: dict[str, str] = {}
|
||||||
async with db.begin():
|
for t in targets:
|
||||||
for t in targets:
|
host = await _ensure_host_for_target(db, t)
|
||||||
host = await _ensure_host_for_target(db, t)
|
tokens[t.name] = await _mint_registration_token(db, host)
|
||||||
tokens[t.name] = await _mint_registration_token(db, host)
|
inv = generate_inventory(targets) # built while ORM objects are still live
|
||||||
inv = generate_inventory(targets)
|
|
||||||
for name, tok in tokens.items():
|
for name, tok in tokens.items():
|
||||||
hv = inv["_meta"]["hostvars"].setdefault(name, {})
|
hv = inv["_meta"]["hostvars"].setdefault(name, {})
|
||||||
hv["steward_url"] = url
|
hv["steward_url"] = url
|
||||||
hv["steward_token"] = tok
|
hv["steward_token"] = tok
|
||||||
inventory_content = _json.dumps(inv)
|
inventory_content = _json.dumps(inv)
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
actor_role = UserRole(session.get("user_role", "viewer"))
|
actor_role = UserRole(session.get("user_role", "viewer"))
|
||||||
run, _source, err = await invoke_capability(
|
run, _source, err = await invoke_capability(
|
||||||
@@ -805,12 +807,13 @@ async def provision_via_ansible():
|
|||||||
targets = await fetch_scope_targets(db, scope)
|
targets = await fetch_scope_targets(db, scope)
|
||||||
if not targets:
|
if not targets:
|
||||||
return _error(400, "no_targets", "No Ansible targets in that scope")
|
return _error(400, "no_targets", "No Ansible targets in that scope")
|
||||||
|
# The read above autobegins the session transaction (SQLAlchemy 2.0), so
|
||||||
|
# mint into it directly and commit — a nested db.begin() would double-begin.
|
||||||
tokens: dict[str, str] = {}
|
tokens: dict[str, str] = {}
|
||||||
async with db.begin():
|
for t in targets:
|
||||||
for t in targets:
|
host = await _ensure_host_for_target(db, t)
|
||||||
host = await _ensure_host_for_target(db, t)
|
tokens[t.name] = await _mint_registration_token(db, host)
|
||||||
tokens[t.name] = await _mint_registration_token(db, host)
|
inv = generate_inventory(targets) # built while ORM objects are still live
|
||||||
inv = generate_inventory(targets)
|
|
||||||
for name, tok in tokens.items():
|
for name, tok in tokens.items():
|
||||||
hv = inv["_meta"]["hostvars"].setdefault(name, {})
|
hv = inv["_meta"]["hostvars"].setdefault(name, {})
|
||||||
hv["steward_url"] = url
|
hv["steward_url"] = url
|
||||||
@@ -821,6 +824,7 @@ async def provision_via_ansible():
|
|||||||
hv["steward_pubkey"] = pubkey
|
hv["steward_pubkey"] = pubkey
|
||||||
hv["steward_user"] = steward_user
|
hv["steward_user"] = steward_user
|
||||||
inventory_content = _json.dumps(inv)
|
inventory_content = _json.dumps(inv)
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
actor_role = UserRole(session.get("user_role", "viewer"))
|
actor_role = UserRole(session.get("user_role", "viewer"))
|
||||||
run, _source, err = await invoke_capability(
|
run, _source, err = await invoke_capability(
|
||||||
|
|||||||
Reference in New Issue
Block a user