feat: add control board replacement workflow
This commit is contained in:
1 parent
62c91a2c3e
commit
bcf1b4e3cf
16 files changed
+2122
-279
No files matched your search
@@ -6,13 +6,15 @@ from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
import psycopg
|
||||
from psycopg.errors import UniqueViolation
|
||||
from psycopg.errors import CheckViolation, LockNotAvailable, UniqueViolation
|
||||
from psycopg.rows import dict_row
|
||||
from psycopg.types.json import Jsonb
|
||||
|
||||
from app.config import settings
|
||||
from app.repairs.exceptions import (
|
||||
ActiveRepairConflict,
|
||||
ControlBoardReplacementConflict,
|
||||
MacOwnershipConflict,
|
||||
RepairPersistenceError,
|
||||
)
|
||||
|
||||
@@ -64,6 +66,26 @@ class RepairRepository:
|
||||
try:
|
||||
yield transaction_repository
|
||||
connection.commit()
|
||||
except UniqueViolation as exc:
|
||||
connection.rollback()
|
||||
if exc.diag.constraint_name in {
|
||||
"uq_miner_mac_ownerships_owner",
|
||||
"uq_miner_network_identities_normalized_mac",
|
||||
"uq_miners_current_normalized_mac",
|
||||
"uq_control_board_installations_current_miner",
|
||||
"uq_control_board_installations_current_mac",
|
||||
}:
|
||||
raise ControlBoardReplacementConflict(
|
||||
"Concurrent control-board replacement or MAC ownership conflict"
|
||||
) from exc
|
||||
raise
|
||||
except CheckViolation as exc:
|
||||
connection.rollback()
|
||||
if exc.diag.constraint_name == "ck_current_control_board_projection":
|
||||
raise ControlBoardReplacementConflict(
|
||||
"Control-board projection would contradict authoritative history"
|
||||
) from exc
|
||||
raise
|
||||
except Exception:
|
||||
connection.rollback()
|
||||
raise
|
||||
@@ -97,7 +119,8 @@ class RepairRepository:
|
||||
with connection.cursor(row_factory=dict_row) as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, lifecycle_status
|
||||
SELECT id, asset_tag, lifecycle_status, current_mac, control_board_sn,
|
||||
firmware_version
|
||||
FROM miners
|
||||
WHERE id = %s
|
||||
FOR UPDATE
|
||||
@@ -107,6 +130,27 @@ class RepairRepository:
|
||||
row = cur.fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
def lock_miner_for_control_board_replacement(
|
||||
self, miner_id: UUID
|
||||
) -> dict | None:
|
||||
with self._connection_scope() as connection:
|
||||
with connection.cursor(row_factory=dict_row) as cur:
|
||||
try:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, asset_tag, lifecycle_status, current_mac,
|
||||
control_board_sn, firmware_version
|
||||
FROM miners WHERE id = %s FOR UPDATE NOWAIT
|
||||
""",
|
||||
(miner_id,),
|
||||
)
|
||||
except LockNotAvailable as exc:
|
||||
raise ControlBoardReplacementConflict(
|
||||
"A control-board replacement is already in progress"
|
||||
) from exc
|
||||
row = cur.fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
def get_active_repair(self, miner_id: UUID) -> dict | None:
|
||||
with self._connection_scope() as connection:
|
||||
with connection.cursor(row_factory=dict_row) as cur:
|
||||
@@ -342,6 +386,217 @@ class RepairRepository:
|
||||
row = cur.fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
def lock_active_repair_for_miner(self, miner_id: UUID) -> dict | None:
|
||||
with self._connection_scope() as connection:
|
||||
with connection.cursor(row_factory=dict_row) as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, miner_id, status, reason, opened_at, opened_by
|
||||
FROM repairs
|
||||
WHERE miner_id = %s AND status = ANY(%s)
|
||||
ORDER BY opened_at, id
|
||||
LIMIT 1
|
||||
FOR UPDATE
|
||||
""",
|
||||
(miner_id, list(ACTIVE_REPAIR_STATUSES)),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
def lock_current_control_board(self, miner_id: UUID) -> dict | None:
|
||||
with self._connection_scope() as connection:
|
||||
with connection.cursor(row_factory=dict_row) as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT * FROM miner_control_board_installations
|
||||
WHERE miner_id = %s AND removed_at IS NULL
|
||||
FOR UPDATE
|
||||
""",
|
||||
(miner_id,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
def create_legacy_control_board(self, miner: Mapping[str, Any]) -> dict:
|
||||
with self._connection_scope(write=True) as connection:
|
||||
with connection.cursor(row_factory=dict_row) as cur:
|
||||
mac_address = miner.get("current_mac")
|
||||
normalized_mac = (
|
||||
"".join(ch for ch in str(mac_address).lower() if ch in "0123456789abcdef")
|
||||
if mac_address else None
|
||||
)
|
||||
if normalized_mac and len(normalized_mac) == 12:
|
||||
mac_address = ":".join(
|
||||
normalized_mac[index:index + 2].upper()
|
||||
for index in range(0, 12, 2)
|
||||
)
|
||||
elif normalized_mac:
|
||||
normalized_mac = None
|
||||
mac_address = None
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO miner_control_board_installations (
|
||||
miner_id, record_origin, mac_address, normalized_mac,
|
||||
board_serial, firmware_version
|
||||
)
|
||||
VALUES (%s, 'legacy_projection', %s, %s, %s, %s)
|
||||
RETURNING *
|
||||
""",
|
||||
(miner["id"], mac_address, normalized_mac,
|
||||
miner.get("control_board_sn"), miner.get("firmware_version")),
|
||||
)
|
||||
return dict(cur.fetchone())
|
||||
|
||||
def claim_mac_ownership(
|
||||
self, normalized_mac: str, miner_id: UUID, actor_user_id: UUID
|
||||
) -> None:
|
||||
with self._connection_scope(write=True) as connection:
|
||||
with connection.cursor(row_factory=dict_row) as cur:
|
||||
cur.execute(
|
||||
"SELECT pg_advisory_xact_lock(hashtextextended(%s, 0))",
|
||||
(normalized_mac,),
|
||||
)
|
||||
cur.execute(
|
||||
"SELECT miner_id FROM miner_mac_ownerships "
|
||||
"WHERE normalized_mac = %s FOR UPDATE",
|
||||
(normalized_mac,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row and row["miner_id"] != miner_id:
|
||||
raise MacOwnershipConflict(normalized_mac)
|
||||
if row is None:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO miner_mac_ownerships (
|
||||
normalized_mac, miner_id, claimed_at, claimed_by
|
||||
) VALUES (%s, %s, now(), %s)
|
||||
""",
|
||||
(normalized_mac, miner_id, actor_user_id),
|
||||
)
|
||||
|
||||
def close_control_board(
|
||||
self, installation_id: UUID, *, repair_id: UUID, actor_user_id: UUID,
|
||||
disposition: str
|
||||
) -> dict:
|
||||
with self._connection_scope(write=True) as connection:
|
||||
with connection.cursor(row_factory=dict_row) as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE miner_control_board_installations
|
||||
SET removed_at = now(), removed_by = %s,
|
||||
removed_repair_id = %s, removed_disposition = %s
|
||||
WHERE id = %s AND removed_at IS NULL
|
||||
RETURNING *
|
||||
""",
|
||||
(actor_user_id, repair_id, disposition, installation_id),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row is None:
|
||||
raise RepairPersistenceError(
|
||||
"Current control-board installation was not closed"
|
||||
)
|
||||
return dict(row)
|
||||
|
||||
def create_control_board_installation(
|
||||
self, *, miner_id: UUID, repair_id: UUID, mac_address: str,
|
||||
normalized_mac: str, board_serial: str | None, board_model: str | None,
|
||||
board_revision: str | None, firmware_version: str | None,
|
||||
replacement_reason: str, notes: str | None, actor_user_id: UUID
|
||||
) -> dict:
|
||||
with self._connection_scope(write=True) as connection:
|
||||
with connection.cursor(row_factory=dict_row) as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO miner_control_board_installations (
|
||||
miner_id, record_origin, installed_repair_id, mac_address,
|
||||
normalized_mac, board_serial, board_model, board_revision,
|
||||
firmware_version, replacement_reason, installed_at,
|
||||
installed_by, notes
|
||||
) VALUES (
|
||||
%s, 'replacement', %s, %s, %s, %s, %s, %s, %s, %s,
|
||||
now(), %s, %s
|
||||
) RETURNING *
|
||||
""",
|
||||
(miner_id, repair_id, mac_address, normalized_mac, board_serial,
|
||||
board_model, board_revision, firmware_version,
|
||||
replacement_reason, actor_user_id, notes),
|
||||
)
|
||||
return dict(cur.fetchone())
|
||||
|
||||
def set_current_control_board(
|
||||
self, miner_id: UUID, *, mac_address: str, board_serial: str | None
|
||||
) -> None:
|
||||
with self._connection_scope(write=True) as connection:
|
||||
with connection.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE miners SET current_mac = %s, control_board_sn = %s,
|
||||
updated_at = now() WHERE id = %s
|
||||
""",
|
||||
(mac_address, board_serial, miner_id),
|
||||
)
|
||||
if cur.rowcount != 1:
|
||||
raise RepairPersistenceError(
|
||||
"Current miner control-board projection was not updated"
|
||||
)
|
||||
|
||||
def ensure_network_identity_history(
|
||||
self, miner_id: UUID, *, mac_address: str, normalized_mac: str
|
||||
) -> None:
|
||||
with self._connection_scope(write=True) as connection:
|
||||
with connection.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO miner_network_identities (
|
||||
miner_id, mac_address, source, is_current,
|
||||
first_seen_at, last_seen_at, associated_at
|
||||
)
|
||||
SELECT %s, %s, 'legacy_projection', false, NULL, NULL, now()
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM miner_network_identities
|
||||
WHERE miner_id = %s
|
||||
AND regexp_replace(lower(mac_address), '[^0-9a-f]', '', 'g') = %s
|
||||
)
|
||||
""",
|
||||
(miner_id, mac_address, miner_id, normalized_mac),
|
||||
)
|
||||
|
||||
def set_current_network_identity(
|
||||
self, miner_id: UUID, *, mac_address: str, normalized_mac: str
|
||||
) -> dict:
|
||||
with self._connection_scope(write=True) as connection:
|
||||
with connection.cursor(row_factory=dict_row) as cur:
|
||||
cur.execute(
|
||||
"UPDATE miner_network_identities SET is_current = false, "
|
||||
"updated_at = now() WHERE miner_id = %s AND is_current",
|
||||
(miner_id,),
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE miner_network_identities
|
||||
SET is_current = true, associated_at = now(), updated_at = now()
|
||||
WHERE miner_id = %s
|
||||
AND regexp_replace(lower(mac_address), '[^0-9a-f]', '', 'g') = %s
|
||||
RETURNING *
|
||||
""",
|
||||
(miner_id, normalized_mac),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row is None:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO miner_network_identities (
|
||||
miner_id, mac_address, source, is_current,
|
||||
first_seen_at, last_seen_at, associated_at
|
||||
) VALUES (%s, %s, 'control_board_replacement', true,
|
||||
NULL, NULL, now())
|
||||
RETURNING *
|
||||
""",
|
||||
(miner_id, mac_address),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
return dict(row)
|
||||
|
||||
def get_repair_detail(self, repair_id: UUID) -> dict | None:
|
||||
with self._connection_scope() as connection:
|
||||
with connection.cursor(row_factory=dict_row) as cur:
|
||||
|
||||
Reference in new issue
Block a user