Initial MinerOps working checkpoint
This commit is contained in:
commit
4c854374e0
103 files changed
+2400932
No files matched your search
+34
@@ -0,0 +1,34 @@
|
||||
# Secrets / environment
|
||||
.env
|
||||
.env.*
|
||||
*.env
|
||||
|
||||
# Backups / dumps / generated data
|
||||
backups/
|
||||
data/
|
||||
logs/
|
||||
imports/
|
||||
*.sql
|
||||
*.sql.gz
|
||||
*.dump
|
||||
*.bak
|
||||
*.csv
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
|
||||
# Docker/runtime
|
||||
*.log
|
||||
tmp/
|
||||
temp/
|
||||
|
||||
# OS/editor
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.vscode/
|
||||
.idea/
|
||||
@@ -0,0 +1,19 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
COPY requirements.txt /app/requirements.txt
|
||||
|
||||
RUN pip install --no-cache-dir --upgrade pip \
|
||||
&& pip install --no-cache-dir -r /app/requirements.txt
|
||||
|
||||
COPY app /app/app
|
||||
COPY alembic.ini /app/alembic.ini
|
||||
COPY alembic /app/alembic
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
@@ -0,0 +1,40 @@
|
||||
[alembic]
|
||||
script_location = alembic
|
||||
prepend_sys_path = .
|
||||
sqlalchemy.url = postgresql+psycopg://minerops:dummy@postgres:5432/minerops
|
||||
|
||||
[post_write_hooks]
|
||||
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARNING
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARNING
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
@@ -0,0 +1,54 @@
|
||||
from logging.config import fileConfig
|
||||
|
||||
from alembic import context
|
||||
|
||||
from app.database import DATABASE_URL, Base
|
||||
|
||||
config = context.config
|
||||
config.set_main_option("sqlalchemy.url", DATABASE_URL)
|
||||
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
target_metadata = Base.metadata
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
context.configure(
|
||||
url=DATABASE_URL,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online() -> None:
|
||||
from sqlalchemy import engine_from_config
|
||||
from sqlalchemy import pool
|
||||
|
||||
configuration = config.get_section(config.config_ini_section, {})
|
||||
configuration["sqlalchemy.url"] = DATABASE_URL
|
||||
|
||||
connectable = engine_from_config(
|
||||
configuration,
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
@@ -0,0 +1,173 @@
|
||||
"""create initial foundation tables
|
||||
|
||||
Revision ID: 0001_foundation
|
||||
Revises:
|
||||
Create Date: 2026-07-08
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
|
||||
revision: str = "0001_foundation"
|
||||
down_revision: Union[str, None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute('CREATE EXTENSION IF NOT EXISTS "pgcrypto";')
|
||||
|
||||
op.create_table(
|
||||
"roles",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
|
||||
sa.Column("name", sa.Text(), nullable=False, unique=True),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"permissions",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
|
||||
sa.Column("key", sa.Text(), nullable=False, unique=True),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"role_permissions",
|
||||
sa.Column("role_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("roles.id", ondelete="CASCADE"), nullable=False),
|
||||
sa.Column("permission_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("permissions.id", ondelete="CASCADE"), nullable=False),
|
||||
sa.PrimaryKeyConstraint("role_id", "permission_id"),
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"users",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
|
||||
sa.Column("username", sa.Text(), nullable=False, unique=True),
|
||||
sa.Column("display_name", sa.Text(), nullable=False),
|
||||
sa.Column("email", sa.Text(), nullable=True),
|
||||
sa.Column("password_hash", sa.Text(), nullable=False),
|
||||
sa.Column("role_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("roles.id"), nullable=False),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.text("true")),
|
||||
sa.Column("last_login_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
|
||||
)
|
||||
|
||||
op.create_index("idx_users_role_id", "users", ["role_id"])
|
||||
op.create_index("idx_users_is_active", "users", ["is_active"])
|
||||
|
||||
op.create_table(
|
||||
"sites",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
|
||||
sa.Column("name", sa.Text(), nullable=False, unique=True),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"rooms",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
|
||||
sa.Column("site_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("sites.id", ondelete="CASCADE"), nullable=False),
|
||||
sa.Column("name", sa.Text(), nullable=False),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("sort_order", sa.Integer(), nullable=False, server_default=sa.text("0")),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
|
||||
sa.UniqueConstraint("site_id", "name", name="uq_rooms_site_name"),
|
||||
)
|
||||
|
||||
op.create_index("idx_rooms_site_id", "rooms", ["site_id"])
|
||||
|
||||
op.create_table(
|
||||
"app_settings",
|
||||
sa.Column("key", sa.Text(), primary_key=True),
|
||||
sa.Column("value_json", postgresql.JSONB(), nullable=False),
|
||||
sa.Column("is_sensitive", sa.Boolean(), nullable=False, server_default=sa.text("false")),
|
||||
sa.Column("updated_by_user_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="SET NULL"), nullable=True),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
|
||||
)
|
||||
|
||||
op.execute("""
|
||||
INSERT INTO roles (name, description) VALUES
|
||||
('Admin', 'Full system administrator'),
|
||||
('Technician', 'Technician user for repair and operational tasks'),
|
||||
('Viewer', 'Read-only dashboard and miner viewer'),
|
||||
('Auditor', 'Read-only audit and reporting user');
|
||||
""")
|
||||
|
||||
op.execute("""
|
||||
INSERT INTO permissions (key, description) VALUES
|
||||
('admin.access', 'Access administrator features'),
|
||||
('users.manage', 'Create and manage users'),
|
||||
('miners.view', 'View miners'),
|
||||
('miners.create', 'Create miners'),
|
||||
('miners.update', 'Update miners'),
|
||||
('miners.delete', 'Delete miners'),
|
||||
('scan_ranges.manage', 'Manage scan ranges'),
|
||||
('scans.run', 'Run scans'),
|
||||
('credentials.manage', 'Manage miner credentials'),
|
||||
('backups.create', 'Create backups'),
|
||||
('exports.create', 'Create exports'),
|
||||
('audit.view', 'View audit logs');
|
||||
""")
|
||||
|
||||
op.execute("""
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
CROSS JOIN permissions p
|
||||
WHERE r.name = 'Admin';
|
||||
""")
|
||||
|
||||
op.execute("""
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.key IN (
|
||||
'miners.view',
|
||||
'miners.update',
|
||||
'scans.run',
|
||||
'exports.create'
|
||||
)
|
||||
WHERE r.name = 'Technician';
|
||||
""")
|
||||
|
||||
op.execute("""
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.key IN ('miners.view')
|
||||
WHERE r.name = 'Viewer';
|
||||
""")
|
||||
|
||||
op.execute("""
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.key IN ('miners.view', 'audit.view', 'exports.create')
|
||||
WHERE r.name = 'Auditor';
|
||||
""")
|
||||
|
||||
op.execute("""
|
||||
INSERT INTO sites (name, description)
|
||||
VALUES ('Main Mining Site', 'Default mining site');
|
||||
""")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("app_settings")
|
||||
op.drop_index("idx_rooms_site_id", table_name="rooms")
|
||||
op.drop_table("rooms")
|
||||
op.drop_table("sites")
|
||||
op.drop_index("idx_users_is_active", table_name="users")
|
||||
op.drop_index("idx_users_role_id", table_name="users")
|
||||
op.drop_table("users")
|
||||
op.drop_table("role_permissions")
|
||||
op.drop_table("permissions")
|
||||
op.drop_table("roles")
|
||||
@@ -0,0 +1,50 @@
|
||||
"""create audit logs table
|
||||
|
||||
Revision ID: 0002_audit_logs
|
||||
Revises: 0001_foundation
|
||||
Create Date: 2026-07-08
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
|
||||
revision: str = "0002_audit_logs"
|
||||
down_revision: Union[str, None] = "0001_foundation"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"audit_logs",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
|
||||
sa.Column("actor_user_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="SET NULL"), nullable=True),
|
||||
sa.Column("action", sa.Text(), nullable=False),
|
||||
sa.Column("entity_type", sa.Text(), nullable=False),
|
||||
sa.Column("entity_id", postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column("old_value_json", postgresql.JSONB(), nullable=True),
|
||||
sa.Column("new_value_json", postgresql.JSONB(), nullable=True),
|
||||
sa.Column("related_miner_asset_id", postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column("ip_address", postgresql.INET(), nullable=True),
|
||||
sa.Column("user_agent", sa.Text(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
|
||||
)
|
||||
|
||||
op.create_index("idx_audit_logs_actor_user_id", "audit_logs", ["actor_user_id"])
|
||||
op.create_index("idx_audit_logs_action", "audit_logs", ["action"])
|
||||
op.create_index("idx_audit_logs_entity", "audit_logs", ["entity_type", "entity_id"])
|
||||
op.create_index("idx_audit_logs_related_miner", "audit_logs", ["related_miner_asset_id"])
|
||||
op.create_index("idx_audit_logs_created_at", "audit_logs", ["created_at"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("idx_audit_logs_created_at", table_name="audit_logs")
|
||||
op.drop_index("idx_audit_logs_related_miner", table_name="audit_logs")
|
||||
op.drop_index("idx_audit_logs_entity", table_name="audit_logs")
|
||||
op.drop_index("idx_audit_logs_action", table_name="audit_logs")
|
||||
op.drop_index("idx_audit_logs_actor_user_id", table_name="audit_logs")
|
||||
op.drop_table("audit_logs")
|
||||
@@ -0,0 +1,62 @@
|
||||
"""create scan ranges table
|
||||
|
||||
Revision ID: 0003_scan_ranges
|
||||
Revises: 0002_audit_logs
|
||||
Create Date: 2026-07-08
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
|
||||
revision: str = "0003_scan_ranges"
|
||||
down_revision: Union[str, None] = "0002_audit_logs"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"scan_ranges",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
|
||||
sa.Column("room_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("rooms.id", ondelete="SET NULL"), nullable=True),
|
||||
sa.Column("name", sa.Text(), nullable=False),
|
||||
sa.Column("range_type", sa.Text(), nullable=False, server_default=sa.text("'cidr'")),
|
||||
sa.Column("cidr", sa.Text(), nullable=True),
|
||||
sa.Column("start_ip", postgresql.INET(), nullable=True),
|
||||
sa.Column("end_ip", postgresql.INET(), nullable=True),
|
||||
sa.Column("enabled", sa.Boolean(), nullable=False, server_default=sa.text("true")),
|
||||
sa.Column("scan_interval_seconds", sa.Integer(), nullable=False, server_default=sa.text("900")),
|
||||
sa.Column("timeout_ms", sa.Integer(), nullable=False, server_default=sa.text("3000")),
|
||||
sa.Column("retry_limit", sa.Integer(), nullable=False, server_default=sa.text("1")),
|
||||
sa.Column("parallelism_limit", sa.Integer(), nullable=False, server_default=sa.text("10")),
|
||||
sa.Column("stagger_ms", sa.Integer(), nullable=False, server_default=sa.text("250")),
|
||||
sa.Column("api_port", sa.Integer(), nullable=False, server_default=sa.text("4028")),
|
||||
sa.Column("last_scan_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("next_scan_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("created_by_user_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="SET NULL"), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
|
||||
sa.CheckConstraint(
|
||||
"""
|
||||
(range_type = 'cidr' AND cidr IS NOT NULL)
|
||||
OR
|
||||
(range_type = 'start_end' AND start_ip IS NOT NULL AND end_ip IS NOT NULL)
|
||||
""",
|
||||
name="scan_range_has_valid_range",
|
||||
),
|
||||
)
|
||||
|
||||
op.create_index("idx_scan_ranges_room_id", "scan_ranges", ["room_id"])
|
||||
op.create_index("idx_scan_ranges_enabled", "scan_ranges", ["enabled"])
|
||||
op.create_index("idx_scan_ranges_next_scan_at", "scan_ranges", ["next_scan_at"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("idx_scan_ranges_next_scan_at", table_name="scan_ranges")
|
||||
op.drop_index("idx_scan_ranges_enabled", table_name="scan_ranges")
|
||||
op.drop_index("idx_scan_ranges_room_id", table_name="scan_ranges")
|
||||
op.drop_table("scan_ranges")
|
||||
@@ -0,0 +1,78 @@
|
||||
"""create scan run and scan result tables
|
||||
|
||||
Revision ID: 0004_scan_runs_results
|
||||
Revises: 0003_scan_ranges
|
||||
Create Date: 2026-07-08
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
|
||||
revision: str = "0004_scan_runs_results"
|
||||
down_revision: Union[str, None] = "0003_scan_ranges"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"scan_runs",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
|
||||
sa.Column("scan_range_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("scan_ranges.id", ondelete="SET NULL"), nullable=True),
|
||||
sa.Column("triggered_by", sa.Text(), nullable=False, server_default=sa.text("'manual'")),
|
||||
sa.Column("triggered_by_user_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="SET NULL"), nullable=True),
|
||||
sa.Column("started_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
|
||||
sa.Column("finished_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("status", sa.Text(), nullable=False, server_default=sa.text("'running'")),
|
||||
sa.Column("total_ips", sa.Integer(), nullable=False, server_default=sa.text("0")),
|
||||
sa.Column("scanned_ips", sa.Integer(), nullable=False, server_default=sa.text("0")),
|
||||
sa.Column("miners_found", sa.Integer(), nullable=False, server_default=sa.text("0")),
|
||||
sa.Column("errors_count", sa.Integer(), nullable=False, server_default=sa.text("0")),
|
||||
sa.Column("notes", sa.Text(), nullable=True),
|
||||
)
|
||||
|
||||
op.create_index("idx_scan_runs_scan_range_id", "scan_runs", ["scan_range_id"])
|
||||
op.create_index("idx_scan_runs_started_at", "scan_runs", ["started_at"])
|
||||
op.create_index("idx_scan_runs_status", "scan_runs", ["status"])
|
||||
|
||||
op.create_table(
|
||||
"scan_results",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
|
||||
sa.Column("scan_run_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("scan_runs.id", ondelete="CASCADE"), nullable=False),
|
||||
sa.Column("ip_address", postgresql.INET(), nullable=False),
|
||||
sa.Column("port", sa.Integer(), nullable=False, server_default=sa.text("4028")),
|
||||
sa.Column("detected", sa.Boolean(), nullable=False, server_default=sa.text("false")),
|
||||
sa.Column("mac_address", sa.Text(), nullable=True),
|
||||
sa.Column("response_ms", sa.Integer(), nullable=True),
|
||||
sa.Column("status", sa.Text(), nullable=False, server_default=sa.text("'unknown'")),
|
||||
sa.Column("error_message", sa.Text(), nullable=True),
|
||||
sa.Column("raw_summary_json", postgresql.JSONB(), nullable=True),
|
||||
sa.Column("raw_version_json", postgresql.JSONB(), nullable=True),
|
||||
sa.Column("raw_pools_json", postgresql.JSONB(), nullable=True),
|
||||
sa.Column("raw_error_json", postgresql.JSONB(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
|
||||
)
|
||||
|
||||
op.create_index("idx_scan_results_scan_run_id", "scan_results", ["scan_run_id"])
|
||||
op.create_index("idx_scan_results_ip_address", "scan_results", ["ip_address"])
|
||||
op.create_index("idx_scan_results_detected", "scan_results", ["detected"])
|
||||
op.create_index("idx_scan_results_mac_address", "scan_results", [sa.text("lower(mac_address)")])
|
||||
op.create_index("idx_scan_results_created_at", "scan_results", ["created_at"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("idx_scan_results_created_at", table_name="scan_results")
|
||||
op.drop_index("idx_scan_results_mac_address", table_name="scan_results")
|
||||
op.drop_index("idx_scan_results_detected", table_name="scan_results")
|
||||
op.drop_index("idx_scan_results_ip_address", table_name="scan_results")
|
||||
op.drop_index("idx_scan_results_scan_run_id", table_name="scan_results")
|
||||
op.drop_table("scan_results")
|
||||
|
||||
op.drop_index("idx_scan_runs_status", table_name="scan_runs")
|
||||
op.drop_index("idx_scan_runs_started_at", table_name="scan_runs")
|
||||
op.drop_index("idx_scan_runs_scan_range_id", table_name="scan_runs")
|
||||
op.drop_table("scan_runs")
|
||||
@@ -0,0 +1,155 @@
|
||||
"""create miner inventory tables
|
||||
|
||||
Revision ID: 0005_miner_inventory
|
||||
Revises: 0004_scan_runs_results
|
||||
Create Date: 2026-07-08
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
|
||||
revision: str = "0005_miner_inventory"
|
||||
down_revision: Union[str, None] = "0004_scan_runs_results"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"miners",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
|
||||
sa.Column("site_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("sites.id", ondelete="SET NULL"), nullable=True),
|
||||
sa.Column("room_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("rooms.id", ondelete="SET NULL"), nullable=True),
|
||||
sa.Column("asset_tag", sa.Text(), nullable=True),
|
||||
sa.Column("current_mac", sa.Text(), nullable=True),
|
||||
sa.Column("current_ip", postgresql.INET(), nullable=True),
|
||||
sa.Column("hostname", sa.Text(), nullable=True),
|
||||
sa.Column("miner_sn", sa.Text(), nullable=True),
|
||||
sa.Column("power_sn", sa.Text(), nullable=True),
|
||||
sa.Column("model", sa.Text(), nullable=True),
|
||||
sa.Column("miner_type", sa.Text(), nullable=True),
|
||||
sa.Column("api_version", sa.Text(), nullable=True),
|
||||
sa.Column("firmware_version", sa.Text(), nullable=True),
|
||||
sa.Column("platform", sa.Text(), nullable=True),
|
||||
sa.Column("chip", sa.Text(), nullable=True),
|
||||
sa.Column("status", sa.Text(), nullable=False, server_default=sa.text("'discovered'")),
|
||||
sa.Column("notes", sa.Text(), nullable=True),
|
||||
sa.Column("first_seen_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
|
||||
sa.Column("last_seen_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
|
||||
)
|
||||
|
||||
op.create_index("idx_miners_site_id", "miners", ["site_id"])
|
||||
op.create_index("idx_miners_room_id", "miners", ["room_id"])
|
||||
op.create_index("idx_miners_current_ip", "miners", ["current_ip"])
|
||||
op.create_index("idx_miners_current_mac", "miners", ["current_mac"])
|
||||
op.create_index("idx_miners_miner_sn", "miners", ["miner_sn"])
|
||||
op.create_index("idx_miners_status", "miners", ["status"])
|
||||
op.create_index("idx_miners_last_seen_at", "miners", ["last_seen_at"])
|
||||
|
||||
op.create_table(
|
||||
"miner_network_identities",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
|
||||
sa.Column("miner_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("miners.id", ondelete="CASCADE"), nullable=False),
|
||||
sa.Column("mac_address", sa.Text(), nullable=False),
|
||||
sa.Column("ip_address", postgresql.INET(), nullable=True),
|
||||
sa.Column("hostname", sa.Text(), nullable=True),
|
||||
sa.Column("source", sa.Text(), nullable=False, server_default=sa.text("'scanner'")),
|
||||
sa.Column("is_current", sa.Boolean(), nullable=False, server_default=sa.text("true")),
|
||||
sa.Column("first_seen_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
|
||||
sa.Column("last_seen_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
|
||||
)
|
||||
|
||||
op.create_index("idx_miner_network_identities_miner_id", "miner_network_identities", ["miner_id"])
|
||||
op.create_index("idx_miner_network_identities_mac_address", "miner_network_identities", ["mac_address"])
|
||||
op.create_index("idx_miner_network_identities_ip_address", "miner_network_identities", ["ip_address"])
|
||||
op.create_index("idx_miner_network_identities_is_current", "miner_network_identities", ["is_current"])
|
||||
|
||||
op.create_table(
|
||||
"miner_hashboards",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
|
||||
sa.Column("miner_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("miners.id", ondelete="CASCADE"), nullable=False),
|
||||
sa.Column("slot", sa.Integer(), nullable=False),
|
||||
sa.Column("pcb_sn", sa.Text(), nullable=True),
|
||||
sa.Column("chip_data", sa.Text(), nullable=True),
|
||||
sa.Column("temperature_c", sa.Numeric(), nullable=True),
|
||||
sa.Column("chip_frequency", sa.Numeric(), nullable=True),
|
||||
sa.Column("mhs_av", sa.Numeric(), nullable=True),
|
||||
sa.Column("mhs_1m", sa.Numeric(), nullable=True),
|
||||
sa.Column("mhs_15m", sa.Numeric(), nullable=True),
|
||||
sa.Column("hs_rt", sa.Numeric(), nullable=True),
|
||||
sa.Column("factory_ghs", sa.Numeric(), nullable=True),
|
||||
sa.Column("effective_chips", sa.Integer(), nullable=True),
|
||||
sa.Column("upfreq_complete", sa.Integer(), nullable=True),
|
||||
sa.Column("raw_json", postgresql.JSONB(), nullable=True),
|
||||
sa.Column("first_seen_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
|
||||
sa.Column("last_seen_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
|
||||
sa.UniqueConstraint("miner_id", "slot", name="uq_miner_hashboards_miner_slot"),
|
||||
)
|
||||
|
||||
op.create_index("idx_miner_hashboards_miner_id", "miner_hashboards", ["miner_id"])
|
||||
op.create_index("idx_miner_hashboards_pcb_sn", "miner_hashboards", ["pcb_sn"])
|
||||
|
||||
op.create_table(
|
||||
"miner_status_snapshots",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
|
||||
sa.Column("miner_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("miners.id", ondelete="CASCADE"), nullable=False),
|
||||
sa.Column("observed_ip", postgresql.INET(), nullable=True),
|
||||
sa.Column("observed_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
|
||||
sa.Column("mhs_av", sa.Numeric(), nullable=True),
|
||||
sa.Column("mhs_1m", sa.Numeric(), nullable=True),
|
||||
sa.Column("mhs_15m", sa.Numeric(), nullable=True),
|
||||
sa.Column("hs_rt", sa.Numeric(), nullable=True),
|
||||
sa.Column("power_w", sa.Numeric(), nullable=True),
|
||||
sa.Column("power_rate_w_per_th", sa.Numeric(), nullable=True),
|
||||
sa.Column("env_temp_c", sa.Numeric(), nullable=True),
|
||||
sa.Column("chip_temp_min_c", sa.Numeric(), nullable=True),
|
||||
sa.Column("chip_temp_max_c", sa.Numeric(), nullable=True),
|
||||
sa.Column("chip_temp_avg_c", sa.Numeric(), nullable=True),
|
||||
sa.Column("fan_speed_in_rpm", sa.Integer(), nullable=True),
|
||||
sa.Column("fan_speed_out_rpm", sa.Integer(), nullable=True),
|
||||
sa.Column("uptime_seconds", sa.BigInteger(), nullable=True),
|
||||
sa.Column("hash_stable", sa.Boolean(), nullable=True),
|
||||
sa.Column("pool_rejected_percent", sa.Numeric(), nullable=True),
|
||||
sa.Column("raw_summary_json", postgresql.JSONB(), nullable=True),
|
||||
sa.Column("raw_version_json", postgresql.JSONB(), nullable=True),
|
||||
sa.Column("raw_miner_info_json", postgresql.JSONB(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
|
||||
)
|
||||
|
||||
op.create_index("idx_miner_status_snapshots_miner_id", "miner_status_snapshots", ["miner_id"])
|
||||
op.create_index("idx_miner_status_snapshots_observed_at", "miner_status_snapshots", ["observed_at"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("idx_miner_status_snapshots_observed_at", table_name="miner_status_snapshots")
|
||||
op.drop_index("idx_miner_status_snapshots_miner_id", table_name="miner_status_snapshots")
|
||||
op.drop_table("miner_status_snapshots")
|
||||
|
||||
op.drop_index("idx_miner_hashboards_pcb_sn", table_name="miner_hashboards")
|
||||
op.drop_index("idx_miner_hashboards_miner_id", table_name="miner_hashboards")
|
||||
op.drop_table("miner_hashboards")
|
||||
|
||||
op.drop_index("idx_miner_network_identities_is_current", table_name="miner_network_identities")
|
||||
op.drop_index("idx_miner_network_identities_ip_address", table_name="miner_network_identities")
|
||||
op.drop_index("idx_miner_network_identities_mac_address", table_name="miner_network_identities")
|
||||
op.drop_index("idx_miner_network_identities_miner_id", table_name="miner_network_identities")
|
||||
op.drop_table("miner_network_identities")
|
||||
|
||||
op.drop_index("idx_miners_last_seen_at", table_name="miners")
|
||||
op.drop_index("idx_miners_status", table_name="miners")
|
||||
op.drop_index("idx_miners_miner_sn", table_name="miners")
|
||||
op.drop_index("idx_miners_current_mac", table_name="miners")
|
||||
op.drop_index("idx_miners_current_ip", table_name="miners")
|
||||
op.drop_index("idx_miners_room_id", table_name="miners")
|
||||
op.drop_index("idx_miners_site_id", table_name="miners")
|
||||
op.drop_table("miners")
|
||||
Whitespace-only changes.
@@ -0,0 +1,26 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
app_name: str = "MinerOps Console"
|
||||
app_env: str = "development"
|
||||
app_timezone: str = "America/Winnipeg"
|
||||
|
||||
postgres_db: str = "minerops"
|
||||
postgres_user: str = "minerops"
|
||||
postgres_password: str
|
||||
postgres_host: str = "postgres"
|
||||
postgres_port: int = 5432
|
||||
|
||||
redis_url: str = "redis://redis:6379/0"
|
||||
|
||||
jwt_secret_key: str
|
||||
jwt_access_token_minutes: int = 30
|
||||
jwt_refresh_token_days: int = 14
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
extra = "ignore"
|
||||
|
||||
|
||||
settings = Settings()
|
||||
@@ -0,0 +1,35 @@
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import DeclarativeBase, sessionmaker
|
||||
|
||||
from app.config import settings
|
||||
|
||||
|
||||
def build_database_url() -> str:
|
||||
return (
|
||||
f"postgresql+psycopg://{settings.postgres_user}:"
|
||||
f"{settings.postgres_password}@{settings.postgres_host}:"
|
||||
f"{settings.postgres_port}/{settings.postgres_db}"
|
||||
)
|
||||
|
||||
|
||||
DATABASE_URL = build_database_url()
|
||||
|
||||
engine = create_engine(DATABASE_URL, pool_pre_ping=True)
|
||||
|
||||
SessionLocal = sessionmaker(
|
||||
autocommit=False,
|
||||
autoflush=False,
|
||||
bind=engine,
|
||||
)
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
def get_db():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
+35893
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
Loaded 100 of 103 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user