Add operational schema and identity roles
This commit is contained in:
1 parent
f5af75f305
commit
f181482463
11 files changed
+2068
-284
No files matched your search
@@ -0,0 +1,478 @@
|
||||
"""capture the operational schema baseline
|
||||
|
||||
Revision ID: 0006_operational_schema_baseline
|
||||
Revises: 0005_miner_inventory
|
||||
Create Date: 2026-07-27
|
||||
|
||||
This migration adopts tables that older MinerOps builds created at runtime.
|
||||
The IF NOT EXISTS clauses allow it to run against both an existing deployment
|
||||
and a new database.
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
revision: str = "0006_operational_schema_baseline"
|
||||
down_revision: Union[str, None] = "0005_miner_inventory"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(
|
||||
"""
|
||||
ALTER TABLE miners
|
||||
ADD COLUMN IF NOT EXISTS manual_details JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
ADD COLUMN IF NOT EXISTS manual_details_updated_at TIMESTAMPTZ,
|
||||
ADD COLUMN IF NOT EXISTS manual_details_updated_by TEXT,
|
||||
ADD COLUMN IF NOT EXISTS psu_model TEXT,
|
||||
ADD COLUMN IF NOT EXISTS customer_sn TEXT,
|
||||
ADD COLUMN IF NOT EXISTS api3_supported BOOLEAN,
|
||||
ADD COLUMN IF NOT EXISTS api3_api_version TEXT,
|
||||
ADD COLUMN IF NOT EXISTS api3_api_switch TEXT,
|
||||
ADD COLUMN IF NOT EXISTS api3_last_seen_at TIMESTAMPTZ,
|
||||
ADD COLUMN IF NOT EXISTS api3_last_error TEXT,
|
||||
ADD COLUMN IF NOT EXISTS configured_performance_mode TEXT,
|
||||
ADD COLUMN IF NOT EXISTS configured_performance_mode_source TEXT,
|
||||
ADD COLUMN IF NOT EXISTS configured_performance_mode_updated_at TIMESTAMPTZ;
|
||||
|
||||
ALTER TABLE miner_status_snapshots
|
||||
ADD COLUMN IF NOT EXISTS ip_address INET,
|
||||
ADD COLUMN IF NOT EXISTS mac_address TEXT,
|
||||
ADD COLUMN IF NOT EXISTS fan_speed_in DOUBLE PRECISION,
|
||||
ADD COLUMN IF NOT EXISTS fan_speed_out DOUBLE PRECISION,
|
||||
ADD COLUMN IF NOT EXISTS elapsed_seconds DOUBLE PRECISION;
|
||||
"""
|
||||
)
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS miner_maintenance_events (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
miner_id UUID NOT NULL REFERENCES miners(id) ON DELETE CASCADE,
|
||||
event_type TEXT NOT NULL,
|
||||
event_status TEXT NOT NULL DEFAULT 'completed',
|
||||
event_title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
event_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
old_mac TEXT,
|
||||
new_mac TEXT,
|
||||
old_ip INET,
|
||||
new_ip INET,
|
||||
component_type TEXT,
|
||||
component_slot TEXT,
|
||||
component_serial TEXT,
|
||||
cost_amount NUMERIC(12, 2),
|
||||
downtime_minutes INTEGER,
|
||||
created_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_miner_maintenance_events_miner_id
|
||||
ON miner_maintenance_events(miner_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_miner_maintenance_events_event_at
|
||||
ON miner_maintenance_events(event_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_miner_maintenance_events_event_type
|
||||
ON miner_maintenance_events(event_type);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS location_positions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
room_id UUID REFERENCES rooms(id) ON DELETE SET NULL,
|
||||
room_name TEXT NOT NULL,
|
||||
rack_name TEXT NOT NULL,
|
||||
shelf_name TEXT NOT NULL,
|
||||
shelf_index INTEGER,
|
||||
position_index INTEGER NOT NULL,
|
||||
position_label TEXT,
|
||||
position_kind TEXT NOT NULL DEFAULT 'miner_slot',
|
||||
position_status TEXT NOT NULL DEFAULT 'available',
|
||||
expected_mac TEXT,
|
||||
miner_id UUID REFERENCES miners(id) ON DELETE SET NULL,
|
||||
equipment_label TEXT,
|
||||
notes TEXT,
|
||||
source_file TEXT,
|
||||
source_sheet TEXT,
|
||||
source_cell TEXT,
|
||||
raw_value TEXT,
|
||||
conflict_reason TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
ALTER TABLE location_positions
|
||||
ADD CONSTRAINT uq_location_positions_slot
|
||||
UNIQUE (room_name, rack_name, shelf_name, position_index);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object OR duplicate_table THEN NULL;
|
||||
END $$;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_location_positions_room
|
||||
ON location_positions(room_name);
|
||||
CREATE INDEX IF NOT EXISTS idx_location_positions_rack
|
||||
ON location_positions(room_name, rack_name);
|
||||
CREATE INDEX IF NOT EXISTS idx_location_positions_expected_mac
|
||||
ON location_positions(expected_mac);
|
||||
CREATE INDEX IF NOT EXISTS idx_location_positions_miner_id
|
||||
ON location_positions(miner_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS miner_location_states (
|
||||
miner_id UUID PRIMARY KEY REFERENCES miners(id) ON DELETE CASCADE,
|
||||
asset_state TEXT NOT NULL DEFAULT 'waiting_placement',
|
||||
location_position_id UUID REFERENCES location_positions(id) ON DELETE SET NULL,
|
||||
state_notes TEXT,
|
||||
updated_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_miner_location_states_asset_state
|
||||
ON miner_location_states(asset_state);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS location_import_issues (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
import_batch_id UUID NOT NULL,
|
||||
issue_type TEXT NOT NULL,
|
||||
severity TEXT NOT NULL DEFAULT 'warning',
|
||||
message TEXT NOT NULL,
|
||||
mac_address TEXT,
|
||||
room_name TEXT,
|
||||
rack_name TEXT,
|
||||
shelf_name TEXT,
|
||||
position_index INTEGER,
|
||||
source_file TEXT,
|
||||
source_cell TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
resolved_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_location_import_issues_batch
|
||||
ON location_import_issues(import_batch_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_location_import_issues_mac
|
||||
ON location_import_issues(mac_address);
|
||||
"""
|
||||
)
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS miner_command_events (
|
||||
id TEXT PRIMARY KEY,
|
||||
miner_id TEXT NOT NULL,
|
||||
command_type TEXT NOT NULL,
|
||||
command_status TEXT NOT NULL,
|
||||
dry_run BOOLEAN NOT NULL DEFAULT true,
|
||||
requested_by_user_id TEXT,
|
||||
requested_by_username TEXT,
|
||||
target_ip TEXT,
|
||||
target_mac TEXT,
|
||||
request_payload JSONB,
|
||||
response_payload JSONB,
|
||||
error_message TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
completed_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_miner_command_events_miner_id_created
|
||||
ON miner_command_events(miner_id, created_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS miner_api_credentials (
|
||||
miner_id TEXT PRIMARY KEY,
|
||||
api_account TEXT NOT NULL DEFAULT 'super',
|
||||
api_password TEXT NOT NULL,
|
||||
password_source TEXT NOT NULL DEFAULT 'minerops',
|
||||
updated_by_username TEXT,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS minerops_site_agents (
|
||||
agent_id TEXT PRIMARY KEY,
|
||||
site_name TEXT NOT NULL,
|
||||
hostname TEXT,
|
||||
version TEXT,
|
||||
ip_address TEXT,
|
||||
last_checkin_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
last_payload JSONB NOT NULL DEFAULT '{}'::jsonb
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS minerops_agent_snapshots (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
agent_id TEXT NOT NULL,
|
||||
site_name TEXT NOT NULL,
|
||||
snapshot_kind TEXT NOT NULL DEFAULT 'miner_poll',
|
||||
received_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
miner_count INTEGER NOT NULL DEFAULT 0,
|
||||
payload JSONB NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_minerops_agent_snapshots_received
|
||||
ON minerops_agent_snapshots(received_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS minerops_agent_command_results (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
agent_id TEXT NOT NULL,
|
||||
site_name TEXT NOT NULL,
|
||||
command_id TEXT,
|
||||
received_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
status TEXT NOT NULL,
|
||||
payload JSONB NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_minerops_agent_command_results_received
|
||||
ON minerops_agent_command_results(received_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS minerops_agent_command_queue (
|
||||
id TEXT PRIMARY KEY,
|
||||
agent_id TEXT NOT NULL,
|
||||
command_type TEXT NOT NULL,
|
||||
command_status TEXT NOT NULL DEFAULT 'pending',
|
||||
requested_by_user_id TEXT,
|
||||
requested_by_username TEXT,
|
||||
request_payload JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
result_payload JSONB,
|
||||
error_message TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
claimed_at TIMESTAMPTZ,
|
||||
completed_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_minerops_agent_command_queue_agent_status
|
||||
ON minerops_agent_command_queue(agent_id, command_status, created_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS minerops_agent_discoveries (
|
||||
id TEXT PRIMARY KEY,
|
||||
agent_id TEXT NOT NULL,
|
||||
site_name TEXT NOT NULL,
|
||||
room_name TEXT,
|
||||
ip_address INET,
|
||||
mac_address TEXT,
|
||||
model TEXT,
|
||||
hostname TEXT,
|
||||
miner_sn TEXT,
|
||||
raw_payload JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
discovery_status TEXT NOT NULL DEFAULT 'new',
|
||||
matched_miner_id UUID,
|
||||
first_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_minerops_agent_discoveries_mac
|
||||
ON minerops_agent_discoveries(mac_address);
|
||||
CREATE INDEX IF NOT EXISTS idx_minerops_agent_discoveries_ip
|
||||
ON minerops_agent_discoveries(ip_address);
|
||||
"""
|
||||
)
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS minerops_scheduled_bulk_actions (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT,
|
||||
target_type TEXT NOT NULL,
|
||||
target_value TEXT,
|
||||
action TEXT NOT NULL DEFAULT 'performance_mode',
|
||||
performance_mode TEXT,
|
||||
repeat_rule TEXT NOT NULL DEFAULT 'once',
|
||||
scheduled_for TIMESTAMPTZ NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
agent_id TEXT NOT NULL DEFAULT 'plexus-windows-agent',
|
||||
requested_by_user_id TEXT,
|
||||
requested_by_username TEXT,
|
||||
queued_command_count INTEGER NOT NULL DEFAULT 0,
|
||||
last_queued_command_count INTEGER NOT NULL DEFAULT 0,
|
||||
run_count INTEGER NOT NULL DEFAULT 0,
|
||||
last_error TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
processed_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_minerops_scheduled_bulk_actions_due
|
||||
ON minerops_scheduled_bulk_actions(status, scheduled_for);
|
||||
CREATE INDEX IF NOT EXISTS idx_minerops_scheduled_bulk_actions_created
|
||||
ON minerops_scheduled_bulk_actions(created_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS miner_pool_snapshots (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
miner_id UUID,
|
||||
ip_address INET,
|
||||
mac_address TEXT,
|
||||
model TEXT,
|
||||
pool_index INTEGER,
|
||||
url TEXT,
|
||||
worker_user TEXT,
|
||||
pool_status TEXT,
|
||||
stratum_active BOOLEAN,
|
||||
accepted DOUBLE PRECISION,
|
||||
rejected DOUBLE PRECISION,
|
||||
stale DOUBLE PRECISION,
|
||||
get_failures DOUBLE PRECISION,
|
||||
remote_failures DOUBLE PRECISION,
|
||||
pool_rejected_percent DOUBLE PRECISION,
|
||||
pool_stale_percent DOUBLE PRECISION,
|
||||
last_share_time_epoch DOUBLE PRECISION,
|
||||
raw_pool_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
agent_id TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
ALTER TABLE miner_pool_snapshots
|
||||
ADD COLUMN IF NOT EXISTS model TEXT,
|
||||
ADD COLUMN IF NOT EXISTS pool_index INTEGER,
|
||||
ADD COLUMN IF NOT EXISTS url TEXT,
|
||||
ADD COLUMN IF NOT EXISTS worker_user TEXT,
|
||||
ADD COLUMN IF NOT EXISTS pool_status TEXT,
|
||||
ADD COLUMN IF NOT EXISTS stratum_active BOOLEAN,
|
||||
ADD COLUMN IF NOT EXISTS accepted DOUBLE PRECISION,
|
||||
ADD COLUMN IF NOT EXISTS rejected DOUBLE PRECISION,
|
||||
ADD COLUMN IF NOT EXISTS stale DOUBLE PRECISION,
|
||||
ADD COLUMN IF NOT EXISTS get_failures DOUBLE PRECISION,
|
||||
ADD COLUMN IF NOT EXISTS remote_failures DOUBLE PRECISION,
|
||||
ADD COLUMN IF NOT EXISTS pool_rejected_percent DOUBLE PRECISION,
|
||||
ADD COLUMN IF NOT EXISTS pool_stale_percent DOUBLE PRECISION,
|
||||
ADD COLUMN IF NOT EXISTS last_share_time_epoch DOUBLE PRECISION,
|
||||
ADD COLUMN IF NOT EXISTS raw_pool_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
ADD COLUMN IF NOT EXISTS agent_id TEXT;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
id_type TEXT;
|
||||
BEGIN
|
||||
SELECT data_type INTO id_type
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = current_schema()
|
||||
AND table_name = 'miner_pool_snapshots'
|
||||
AND column_name = 'id';
|
||||
|
||||
IF id_type = 'text' THEN
|
||||
ALTER TABLE miner_pool_snapshots
|
||||
ALTER COLUMN id SET DEFAULT (gen_random_uuid())::text;
|
||||
ELSIF id_type = 'uuid' THEN
|
||||
ALTER TABLE miner_pool_snapshots
|
||||
ALTER COLUMN id SET DEFAULT gen_random_uuid();
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_miner_pool_snapshots_miner_created
|
||||
ON miner_pool_snapshots(miner_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_miner_pool_snapshots_ip_created
|
||||
ON miner_pool_snapshots(ip_address, created_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS miner_api3_enrichment_snapshots (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
miner_id UUID,
|
||||
agent_id TEXT,
|
||||
site_name TEXT,
|
||||
ip_address INET,
|
||||
mac_address TEXT,
|
||||
api_supported BOOLEAN,
|
||||
api_error TEXT,
|
||||
api_version TEXT,
|
||||
api_switch TEXT,
|
||||
model TEXT,
|
||||
miner_sn TEXT,
|
||||
power_sn TEXT,
|
||||
psu_model TEXT,
|
||||
device_info JSONB,
|
||||
miner_setting JSONB,
|
||||
miner_status JSONB,
|
||||
fan_setting JSONB,
|
||||
custom_data JSONB,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_miner_api3_enrichment_miner_created
|
||||
ON miner_api3_enrichment_snapshots(miner_id, created_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS miner_api3_hashboard_snapshots (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
miner_id UUID,
|
||||
ip_address INET,
|
||||
mac_address TEXT,
|
||||
slot INTEGER,
|
||||
pcb_sn TEXT,
|
||||
hash_average_ths DOUBLE PRECISION,
|
||||
factory_hash_ths DOUBLE PRECISION,
|
||||
freq DOUBLE PRECISION,
|
||||
effective_chips INTEGER,
|
||||
chip_temp_min_c DOUBLE PRECISION,
|
||||
chip_temp_avg_c DOUBLE PRECISION,
|
||||
chip_temp_max_c DOUBLE PRECISION,
|
||||
raw_hashboard_json JSONB,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_miner_api3_hashboards_miner_created
|
||||
ON miner_api3_hashboard_snapshots(miner_id, created_at DESC);
|
||||
"""
|
||||
)
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS miner_status_hourly_rollups (
|
||||
miner_id UUID NOT NULL REFERENCES miners(id) ON DELETE CASCADE,
|
||||
bucket_at TIMESTAMPTZ NOT NULL,
|
||||
sample_count BIGINT NOT NULL,
|
||||
avg_mhs_av DOUBLE PRECISION,
|
||||
max_mhs_av DOUBLE PRECISION,
|
||||
avg_power_w DOUBLE PRECISION,
|
||||
max_chip_temp_c DOUBLE PRECISION,
|
||||
avg_chip_temp_c DOUBLE PRECISION,
|
||||
avg_pool_rejected_percent DOUBLE PRECISION,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (miner_id, bucket_at)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_miner_status_hourly_bucket
|
||||
ON miner_status_hourly_rollups(bucket_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS miner_status_daily_rollups (
|
||||
miner_id UUID NOT NULL REFERENCES miners(id) ON DELETE CASCADE,
|
||||
bucket_date DATE NOT NULL,
|
||||
sample_count BIGINT NOT NULL,
|
||||
avg_mhs_av DOUBLE PRECISION,
|
||||
max_mhs_av DOUBLE PRECISION,
|
||||
avg_power_w DOUBLE PRECISION,
|
||||
max_chip_temp_c DOUBLE PRECISION,
|
||||
avg_chip_temp_c DOUBLE PRECISION,
|
||||
avg_pool_rejected_percent DOUBLE PRECISION,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (miner_id, bucket_date)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_miner_status_daily_bucket
|
||||
ON miner_status_daily_rollups(bucket_date);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS minerops_retention_runs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
mode TEXT NOT NULL,
|
||||
raw_status_retention_days INTEGER NOT NULL,
|
||||
hourly_rollup_rows_before BIGINT NOT NULL DEFAULT 0,
|
||||
hourly_rollup_rows_after BIGINT NOT NULL DEFAULT 0,
|
||||
daily_rollup_rows_before BIGINT NOT NULL DEFAULT 0,
|
||||
daily_rollup_rows_after BIGINT NOT NULL DEFAULT 0,
|
||||
raw_status_rows_before BIGINT NOT NULL DEFAULT 0,
|
||||
raw_status_rows_after BIGINT NOT NULL DEFAULT 0,
|
||||
raw_status_rows_eligible_delete BIGINT NOT NULL DEFAULT 0,
|
||||
raw_status_rows_deleted BIGINT NOT NULL DEFAULT 0,
|
||||
started_at TIMESTAMPTZ NOT NULL,
|
||||
finished_at TIMESTAMPTZ NOT NULL,
|
||||
notes TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_minerops_retention_runs_started
|
||||
ON minerops_retention_runs(started_at DESC);
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
raise RuntimeError(
|
||||
"0006 adopts pre-existing operational tables and is intentionally irreversible"
|
||||
)
|
||||
@@ -0,0 +1,161 @@
|
||||
"""add application roles and permission matrix
|
||||
|
||||
Revision ID: 0007_identity_and_roles
|
||||
Revises: 0006_operational_schema_baseline
|
||||
Create Date: 2026-07-27
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
revision: str = "0007_identity_and_roles"
|
||||
down_revision: Union[str, None] = "0006_operational_schema_baseline"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
PERMISSIONS = (
|
||||
("dashboard.view", "View fleet dashboards"),
|
||||
("miners.view", "View miners and telemetry"),
|
||||
("miners.update", "Update miner inventory and details"),
|
||||
("miners.control", "Run approved miner control operations"),
|
||||
("repairs.view", "View repair queues and history"),
|
||||
("repairs.manage", "Manage repairs, notes, parts, and status"),
|
||||
("locations.view", "View rooms, racks, shelves, and placements"),
|
||||
("locations.manage", "Manage layouts and miner placements"),
|
||||
("agents.view", "View site agents and command status"),
|
||||
("agents.manage", "Queue and manage site-agent work"),
|
||||
("system_health.view", "View system health and retention status"),
|
||||
("users.manage", "Create and manage MinerOps accounts"),
|
||||
("credentials.manage", "Manage protected miner credentials"),
|
||||
("scan_ranges.manage", "Manage scan ranges"),
|
||||
("scans.run", "Run discovery and polling operations"),
|
||||
("exports.create", "Export operational data"),
|
||||
("audit.view", "View audit history"),
|
||||
)
|
||||
|
||||
ROLE_PERMISSIONS = {
|
||||
"Manager": (
|
||||
"dashboard.view",
|
||||
"miners.view",
|
||||
"miners.update",
|
||||
"miners.control",
|
||||
"repairs.view",
|
||||
"repairs.manage",
|
||||
"locations.view",
|
||||
"locations.manage",
|
||||
"agents.view",
|
||||
"agents.manage",
|
||||
"system_health.view",
|
||||
"scan_ranges.manage",
|
||||
"scans.run",
|
||||
"exports.create",
|
||||
"audit.view",
|
||||
),
|
||||
"Technician": (
|
||||
"dashboard.view",
|
||||
"miners.view",
|
||||
"miners.update",
|
||||
"miners.control",
|
||||
"repairs.view",
|
||||
"repairs.manage",
|
||||
"locations.view",
|
||||
"locations.manage",
|
||||
"agents.view",
|
||||
"scans.run",
|
||||
"exports.create",
|
||||
),
|
||||
"Read Only": (
|
||||
"dashboard.view",
|
||||
"miners.view",
|
||||
"repairs.view",
|
||||
"locations.view",
|
||||
"agents.view",
|
||||
"system_health.view",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
connection = op.get_bind()
|
||||
|
||||
for key, description in PERMISSIONS:
|
||||
connection.exec_driver_sql(
|
||||
"""
|
||||
INSERT INTO permissions (key, description)
|
||||
VALUES (%s, %s)
|
||||
ON CONFLICT (key) DO UPDATE
|
||||
SET description = EXCLUDED.description
|
||||
""",
|
||||
(key, description),
|
||||
)
|
||||
|
||||
connection.exec_driver_sql(
|
||||
"""
|
||||
INSERT INTO roles (name, description)
|
||||
VALUES
|
||||
('Manager', 'Operational manager with fleet and workflow control'),
|
||||
('Read Only', 'Read-only access to operational views')
|
||||
ON CONFLICT (name) DO UPDATE
|
||||
SET description = EXCLUDED.description
|
||||
"""
|
||||
)
|
||||
|
||||
connection.exec_driver_sql(
|
||||
"""
|
||||
UPDATE roles
|
||||
SET description = CASE name
|
||||
WHEN 'Admin' THEN 'Full MinerOps administrator'
|
||||
WHEN 'Technician' THEN 'Technician for repair and fleet operations'
|
||||
ELSE description
|
||||
END
|
||||
WHERE name IN ('Admin', 'Technician')
|
||||
"""
|
||||
)
|
||||
|
||||
connection.exec_driver_sql(
|
||||
"""
|
||||
UPDATE users
|
||||
SET role_id = (SELECT id FROM roles WHERE name = 'Read Only')
|
||||
WHERE role_id IN (
|
||||
SELECT id FROM roles WHERE name IN ('Viewer', 'Auditor')
|
||||
)
|
||||
"""
|
||||
)
|
||||
connection.exec_driver_sql(
|
||||
"DELETE FROM role_permissions WHERE role_id IN (SELECT id FROM roles WHERE name IN ('Viewer', 'Auditor'))"
|
||||
)
|
||||
connection.exec_driver_sql("DELETE FROM roles WHERE name IN ('Viewer', 'Auditor')")
|
||||
|
||||
connection.exec_driver_sql(
|
||||
"DELETE FROM role_permissions WHERE role_id IN (SELECT id FROM roles WHERE name IN ('Admin', 'Manager', 'Technician', 'Read Only'))"
|
||||
)
|
||||
connection.exec_driver_sql(
|
||||
"""
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT roles.id, permissions.id
|
||||
FROM roles
|
||||
CROSS JOIN permissions
|
||||
WHERE roles.name = 'Admin'
|
||||
ON CONFLICT DO NOTHING
|
||||
"""
|
||||
)
|
||||
|
||||
for role_name, keys in ROLE_PERMISSIONS.items():
|
||||
connection.exec_driver_sql(
|
||||
"""
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT roles.id, permissions.id
|
||||
FROM roles
|
||||
JOIN permissions ON permissions.key = ANY(%s)
|
||||
WHERE roles.name = %s
|
||||
ON CONFLICT DO NOTHING
|
||||
""",
|
||||
(list(keys), role_name),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
raise RuntimeError("0007 changes live role assignments and is intentionally irreversible")
|
||||
Reference in new issue
Block a user