260 lines
9.7 KiB
Python
260 lines
9.7 KiB
Python
"""route pre-Zone Agent installations without rewriting ownership history
|
|
|
|
Revision ID: 0018_legacy_agent_routing
|
|
Revises: 0017_agent_enrollment
|
|
Create Date: 2026-08-28
|
|
"""
|
|
|
|
from typing import Sequence, Union
|
|
|
|
import sqlalchemy as sa
|
|
from alembic import op
|
|
|
|
|
|
revision: str = "0018_legacy_agent_routing"
|
|
down_revision: Union[str, None] = "0017_agent_enrollment"
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
depends_on: Union[str, Sequence[str], None] = None
|
|
|
|
DEFAULT_ZONE_NAME = "Default Zone"
|
|
|
|
# Every preflight and the eventual Site UPDATE use this exact resolver. An
|
|
# exact trim/case-folded name match wins. The sole-Site fallback applies only
|
|
# when there are zero exact matches and exactly one durable Site in the whole
|
|
# database. Ambiguous exact matches and unresolved multi-Site installations
|
|
# deliberately produce NULL resolved_site_id and fail before any UPDATE.
|
|
RESOLVED_AGENT_SITES_CTE = """
|
|
site_catalog AS (
|
|
SELECT count(*) AS site_count,
|
|
(array_agg(id ORDER BY id))[1] AS sole_site_id
|
|
FROM sites
|
|
),
|
|
unassigned_exact_matches AS (
|
|
SELECT agent.agent_id,
|
|
agent.site_name,
|
|
count(site.id) AS exact_match_count,
|
|
(
|
|
array_agg(site.id ORDER BY site.id)
|
|
FILTER (WHERE site.id IS NOT NULL)
|
|
)[1] AS exact_site_id
|
|
FROM minerops_site_agents AS agent
|
|
LEFT JOIN sites AS site
|
|
ON lower(btrim(site.name)) = lower(btrim(agent.site_name))
|
|
WHERE agent.site_id IS NULL
|
|
GROUP BY agent.agent_id, agent.site_name
|
|
),
|
|
resolved_unassigned_agents AS (
|
|
SELECT match.agent_id,
|
|
match.site_name,
|
|
match.exact_match_count,
|
|
catalog.site_count,
|
|
CASE
|
|
WHEN match.exact_match_count = 1
|
|
THEN match.exact_site_id
|
|
WHEN match.exact_match_count = 0
|
|
AND catalog.site_count = 1
|
|
THEN catalog.sole_site_id
|
|
ELSE NULL
|
|
END AS resolved_site_id
|
|
FROM unassigned_exact_matches AS match
|
|
CROSS JOIN site_catalog AS catalog
|
|
)
|
|
"""
|
|
|
|
|
|
def _format_rows(rows: list[tuple]) -> str:
|
|
return ", ".join("/".join(repr(str(value)) for value in row) for row in rows)
|
|
|
|
|
|
def upgrade() -> None:
|
|
connection = op.get_bind()
|
|
|
|
# Keep every preflight result stable until the corresponding routing and
|
|
# history updates commit. Production must still take the Hub and all queue
|
|
# producers offline before migration; these locks are the database-level
|
|
# backstop against an accidental concurrent Site, Zone, Agent, snapshot, or
|
|
# discovery write changing an already-checked assumption.
|
|
connection.execute(
|
|
sa.text(
|
|
"""
|
|
LOCK TABLE
|
|
agent_zones,
|
|
minerops_agent_discoveries,
|
|
minerops_agent_snapshots,
|
|
minerops_site_agents,
|
|
sites
|
|
IN SHARE ROW EXCLUSIVE MODE
|
|
"""
|
|
)
|
|
)
|
|
|
|
# Run every fail-closed preflight before the first UPDATE. Site resolution
|
|
# is intentionally limited to legacy rows without an explicit Site
|
|
# assignment; explicit site_id values are never candidates for replacement.
|
|
site_match_failures = connection.execute(
|
|
sa.text(
|
|
f"""
|
|
WITH {RESOLVED_AGENT_SITES_CTE}
|
|
SELECT agent_id, site_name, exact_match_count, site_count
|
|
FROM resolved_unassigned_agents
|
|
WHERE resolved_site_id IS NULL
|
|
ORDER BY agent_id
|
|
"""
|
|
)
|
|
).all()
|
|
if site_match_failures:
|
|
raise RuntimeError(
|
|
"Cannot route legacy Agent installations: each unassigned Agent "
|
|
"must have exactly one Site name match after trim/case folding, "
|
|
"or zero exact matches while the database contains exactly one "
|
|
"Site. Ambiguous exact matches and unmatched multi-Site "
|
|
"installations fail closed. Failing "
|
|
"agent/site/exact-match-count/site-count rows: "
|
|
f"{_format_rows(list(site_match_failures))}. Assign site_id "
|
|
"explicitly, make Site names unambiguous, or identify the correct "
|
|
"Site before retrying; no "
|
|
"Agent, miner, MAC, queue, snapshot, or discovery data was changed."
|
|
)
|
|
|
|
default_zone_failures = connection.execute(
|
|
sa.text(
|
|
f"""
|
|
WITH {RESOLVED_AGENT_SITES_CTE},
|
|
effective_agents AS (
|
|
SELECT agent.agent_id,
|
|
coalesce(
|
|
agent.site_id,
|
|
resolved.resolved_site_id
|
|
) AS site_id
|
|
FROM minerops_site_agents AS agent
|
|
LEFT JOIN resolved_unassigned_agents AS resolved
|
|
ON resolved.agent_id = agent.agent_id
|
|
WHERE agent.agent_zone_id IS NULL
|
|
)
|
|
SELECT agent.agent_id, agent.site_id::text, count(zone.id)
|
|
FROM effective_agents AS agent
|
|
LEFT JOIN agent_zones AS zone
|
|
ON zone.site_id = agent.site_id
|
|
AND zone.is_enabled
|
|
AND lower(btrim(zone.name)) = lower(:default_name)
|
|
GROUP BY agent.agent_id, agent.site_id
|
|
HAVING count(zone.id) <> 1
|
|
ORDER BY agent.agent_id
|
|
"""
|
|
),
|
|
{"default_name": DEFAULT_ZONE_NAME},
|
|
).all()
|
|
if default_zone_failures:
|
|
raise RuntimeError(
|
|
"Cannot route legacy Agent installations: each Agent without an "
|
|
"explicit Zone must have exactly one enabled Default Zone in its "
|
|
"effective Site. Failing agent/site/count rows: "
|
|
f"{_format_rows(list(default_zone_failures))}. Repair the Site's "
|
|
"Default Zone or assign agent_zone_id explicitly before retrying; "
|
|
"no data was changed."
|
|
)
|
|
|
|
zone_conflicts = connection.execute(
|
|
sa.text(
|
|
f"""
|
|
WITH {RESOLVED_AGENT_SITES_CTE},
|
|
targets AS (
|
|
SELECT agent.agent_id, zone.id AS agent_zone_id
|
|
FROM minerops_site_agents AS agent
|
|
LEFT JOIN resolved_unassigned_agents AS resolved
|
|
ON resolved.agent_id = agent.agent_id
|
|
JOIN agent_zones AS zone
|
|
ON zone.site_id = coalesce(
|
|
agent.site_id,
|
|
resolved.resolved_site_id
|
|
)
|
|
AND zone.is_enabled
|
|
AND lower(btrim(zone.name)) = lower(:default_name)
|
|
WHERE agent.agent_zone_id IS NULL
|
|
)
|
|
SELECT target.agent_zone_id::text,
|
|
string_agg(target.agent_id, ',' ORDER BY target.agent_id),
|
|
coalesce(string_agg(occupant.agent_id, ','
|
|
ORDER BY occupant.agent_id), '')
|
|
FROM targets AS target
|
|
LEFT JOIN minerops_site_agents AS occupant
|
|
ON occupant.agent_zone_id = target.agent_zone_id
|
|
GROUP BY target.agent_zone_id
|
|
HAVING count(DISTINCT target.agent_id) > 1
|
|
OR count(occupant.agent_id) > 0
|
|
ORDER BY target.agent_zone_id
|
|
"""
|
|
),
|
|
{"default_name": DEFAULT_ZONE_NAME},
|
|
).all()
|
|
if zone_conflicts:
|
|
raise RuntimeError(
|
|
"Cannot route legacy Agent installations: one-Agent-per-Zone would "
|
|
"be violated. Failing zone/target Agents/current Agent rows: "
|
|
f"{_format_rows(list(zone_conflicts))}. Assign distinct enabled "
|
|
"Zones explicitly before retrying; no data was changed."
|
|
)
|
|
|
|
connection.execute(
|
|
sa.text(
|
|
f"""
|
|
WITH {RESOLVED_AGENT_SITES_CTE}
|
|
UPDATE minerops_site_agents AS agent
|
|
SET site_id = resolved.resolved_site_id
|
|
FROM resolved_unassigned_agents AS resolved
|
|
WHERE agent.agent_id = resolved.agent_id
|
|
AND agent.site_id IS NULL
|
|
AND resolved.resolved_site_id IS NOT NULL
|
|
"""
|
|
)
|
|
)
|
|
connection.execute(
|
|
sa.text(
|
|
"""
|
|
UPDATE minerops_site_agents AS agent
|
|
SET agent_zone_id = zone.id
|
|
FROM agent_zones AS zone
|
|
WHERE agent.site_id = zone.site_id
|
|
AND zone.is_enabled
|
|
AND lower(btrim(zone.name)) = lower(:default_name)
|
|
AND agent.agent_zone_id IS NULL
|
|
"""
|
|
),
|
|
{"default_name": DEFAULT_ZONE_NAME},
|
|
)
|
|
connection.execute(
|
|
sa.text(
|
|
"""
|
|
UPDATE minerops_agent_snapshots AS snapshot
|
|
SET agent_zone_id = agent.agent_zone_id
|
|
FROM minerops_site_agents AS agent
|
|
WHERE snapshot.agent_id = agent.agent_id
|
|
AND snapshot.agent_zone_id IS NULL
|
|
AND agent.agent_zone_id IS NOT NULL
|
|
"""
|
|
)
|
|
)
|
|
connection.execute(
|
|
sa.text(
|
|
"""
|
|
UPDATE minerops_agent_discoveries AS discovery
|
|
SET agent_zone_id = agent.agent_zone_id
|
|
FROM minerops_site_agents AS agent
|
|
WHERE discovery.agent_id = agent.agent_id
|
|
AND discovery.agent_zone_id IS NULL
|
|
AND agent.agent_zone_id IS NOT NULL
|
|
"""
|
|
)
|
|
)
|
|
|
|
|
|
def downgrade() -> None:
|
|
# There is no durable, trustworthy way to distinguish values assigned here
|
|
# from assignments made by an operator after upgrade. Refuse instead of
|
|
# erasing potentially explicit production routing or historical attribution.
|
|
raise RuntimeError(
|
|
"Cannot downgrade 0018 automatically: legacy Site/Zone routing and "
|
|
"snapshot/discovery attribution may now be production data. Restore the "
|
|
"pre-upgrade database backup for rollback; no data was changed."
|
|
)
|