feat(16-01): add devices SNMP columns and snmp_metrics hypertable
- devices table: device_type (default 'routeros'), snmp_port (default 161), snmp_version, snmp_profile_id FK -> snmp_profiles, credential_profile_id FK -> credential_profiles, with lock_timeout = 3s for safe ALTER - snmp_metrics: hypertable with 90-day retention, composite index on (device_id, metric_name, time DESC), RLS with tenant isolation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
86
backend/alembic/versions/039_devices_snmp_columns.py
Normal file
86
backend/alembic/versions/039_devices_snmp_columns.py
Normal file
@@ -0,0 +1,86 @@
|
||||
"""Add SNMP columns to devices table.
|
||||
|
||||
Revision ID: 039
|
||||
Revises: 038
|
||||
Create Date: 2026-03-21
|
||||
|
||||
Adds device_type (default 'routeros' for backward compatibility),
|
||||
snmp_port, snmp_version, snmp_profile_id FK, and credential_profile_id FK.
|
||||
|
||||
Uses lock_timeout = 3s to fail fast rather than queue behind long-running
|
||||
queries. Each ALTER TABLE ADD COLUMN with a non-volatile DEFAULT does NOT
|
||||
rewrite the table in PostgreSQL 11+ -- the default is stored in pg_attribute
|
||||
and applied on read, so this is a metadata-only change.
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision = "039"
|
||||
down_revision = "038"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
|
||||
# Fail fast if devices table is locked by another transaction
|
||||
conn.execute(sa.text("SET lock_timeout = '3s'"))
|
||||
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"ALTER TABLE devices"
|
||||
" ADD COLUMN device_type TEXT NOT NULL DEFAULT 'routeros'"
|
||||
)
|
||||
)
|
||||
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"ALTER TABLE devices"
|
||||
" ADD COLUMN snmp_port INTEGER DEFAULT 161"
|
||||
)
|
||||
)
|
||||
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"ALTER TABLE devices"
|
||||
" ADD COLUMN snmp_version TEXT"
|
||||
)
|
||||
)
|
||||
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"ALTER TABLE devices"
|
||||
" ADD COLUMN snmp_profile_id UUID"
|
||||
" REFERENCES snmp_profiles(id) ON DELETE SET NULL"
|
||||
)
|
||||
)
|
||||
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"ALTER TABLE devices"
|
||||
" ADD COLUMN credential_profile_id UUID"
|
||||
" REFERENCES credential_profiles(id) ON DELETE SET NULL"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
|
||||
conn.execute(
|
||||
sa.text("ALTER TABLE devices DROP COLUMN IF EXISTS credential_profile_id")
|
||||
)
|
||||
conn.execute(
|
||||
sa.text("ALTER TABLE devices DROP COLUMN IF EXISTS snmp_profile_id")
|
||||
)
|
||||
conn.execute(
|
||||
sa.text("ALTER TABLE devices DROP COLUMN IF EXISTS snmp_version")
|
||||
)
|
||||
conn.execute(
|
||||
sa.text("ALTER TABLE devices DROP COLUMN IF EXISTS snmp_port")
|
||||
)
|
||||
conn.execute(
|
||||
sa.text("ALTER TABLE devices DROP COLUMN IF EXISTS device_type")
|
||||
)
|
||||
80
backend/alembic/versions/040_snmp_metrics_hypertable.py
Normal file
80
backend/alembic/versions/040_snmp_metrics_hypertable.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""Create snmp_metrics hypertable for custom SNMP metric storage.
|
||||
|
||||
Revision ID: 040
|
||||
Revises: 039
|
||||
Create Date: 2026-03-21
|
||||
|
||||
Stores OID data that does not map to the standard interface_metrics or
|
||||
health_metrics hypertables (e.g., UPS battery voltage, vendor-specific
|
||||
counters, custom profile OIDs). Structured as a flexible key-value
|
||||
time-series: metric_name + metric_group identify the series, value_numeric
|
||||
and value_text hold typed values, index_value tracks SNMP table row index.
|
||||
|
||||
90-day retention matches existing hypertable policy. RLS enforces
|
||||
tenant isolation.
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision = "040"
|
||||
down_revision = "039"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE TABLE snmp_metrics (
|
||||
time TIMESTAMPTZ NOT NULL,
|
||||
device_id UUID NOT NULL,
|
||||
tenant_id UUID NOT NULL,
|
||||
metric_name TEXT NOT NULL,
|
||||
metric_group TEXT NOT NULL,
|
||||
value_numeric DOUBLE PRECISION,
|
||||
value_text TEXT,
|
||||
oid TEXT NOT NULL,
|
||||
index_value TEXT
|
||||
)
|
||||
""")
|
||||
)
|
||||
|
||||
conn.execute(sa.text("SELECT create_hypertable('snmp_metrics', 'time')"))
|
||||
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"SELECT add_retention_policy('snmp_metrics', INTERVAL '90 days')"
|
||||
)
|
||||
)
|
||||
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE INDEX idx_snmp_metrics_device_metric_time
|
||||
ON snmp_metrics (device_id, metric_name, time DESC)
|
||||
""")
|
||||
)
|
||||
|
||||
conn.execute(
|
||||
sa.text("ALTER TABLE snmp_metrics ENABLE ROW LEVEL SECURITY")
|
||||
)
|
||||
conn.execute(
|
||||
sa.text("ALTER TABLE snmp_metrics FORCE ROW LEVEL SECURITY")
|
||||
)
|
||||
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE POLICY snmp_metrics_tenant_isolation
|
||||
ON snmp_metrics
|
||||
USING (
|
||||
tenant_id::text = current_setting('app.current_tenant', true)
|
||||
OR current_setting('app.current_tenant', true) = 'super_admin'
|
||||
)
|
||||
""")
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("snmp_metrics")
|
||||
Reference in New Issue
Block a user