diff --git a/backend/alembic/versions/039_devices_snmp_columns.py b/backend/alembic/versions/039_devices_snmp_columns.py new file mode 100644 index 0000000..c83cd23 --- /dev/null +++ b/backend/alembic/versions/039_devices_snmp_columns.py @@ -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") + ) diff --git a/backend/alembic/versions/040_snmp_metrics_hypertable.py b/backend/alembic/versions/040_snmp_metrics_hypertable.py new file mode 100644 index 0000000..9bdeee6 --- /dev/null +++ b/backend/alembic/versions/040_snmp_metrics_hypertable.py @@ -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")