#!/usr/bin/env python3 """ setup_timescaledb.py Initializes TimescaleDB tables and extensions for MorphGuard metrics collection. This script should be run once to set up the database schema. By default, it uses the database connection settings from config.py. You can override these settings with command-line arguments. """ import os import sys import argparse import psycopg2 from psycopg2 import sql import logging # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Add project root to Python path for imports sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) def create_api_keys_table(cursor): """Create the api_keys table if it doesn't exist.""" print("Checking api_keys table...") cursor.execute(""" CREATE TABLE IF NOT EXISTS api_keys ( id SERIAL PRIMARY KEY, key_hash TEXT UNIQUE NOT NULL, name TEXT, created_at TIMESTAMPTZ DEFAULT NOW(), is_active BOOLEAN DEFAULT TRUE, permissions TEXT[] ); """) print("api_keys table checked/created.") def create_metrics_tables(host, port, dbname, user, password): """ Create TimescaleDB tables and indexes for metrics collection """ conn = None try: # Connect to the database print(f"Connecting to database {dbname} on {host}:{port} as {user}...") # Build connection parameters; use socket for localhost to avoid TCP hangs # Build connection parameters (always use host so we get MD5 auth) conn_params = { 'host': host, 'port': port, 'dbname': dbname, 'user': user, 'password': password, 'connect_timeout': 10, 'sslmode': 'disable', # Disable SSL for development } conn = psycopg2.connect(**conn_params) cursor = conn.cursor() conn.autocommit = True # Prevent DDL from hanging indefinitely: set short lock & statement timeouts try: cursor.execute("SET lock_timeout = '5s';") cursor.execute("SET statement_timeout = '5min';") except psycopg2.Error: # ignore if not supported pass # Migration/version-control: track schema version to avoid repeated setup TARGET_SCHEMA_VERSION = 7 # bump version to add cloud connector metrics tables # Ensure versioning table exists cursor.execute(""" CREATE TABLE IF NOT EXISTS schema_version ( version INTEGER NOT NULL, updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); """) # Read current version cursor.execute("SELECT version FROM schema_version ORDER BY updated_at DESC LIMIT 1;") row = cursor.fetchone() current_version = row[0] if row else 0 if current_version >= TARGET_SCHEMA_VERSION: print(f"Schema is already at version {current_version}, skipping setup.") return # Otherwise proceed with one-time schema creation/migration # Check if TimescaleDB extension exists cursor.execute("SELECT extname FROM pg_extension WHERE extname = 'timescaledb';") if cursor.fetchone() is None: print("Enabling TimescaleDB extension...") cursor.execute("CREATE EXTENSION IF NOT EXISTS timescaledb;") print("TimescaleDB extension enabled successfully.") else: print("TimescaleDB extension already exists.") # Create system metrics table cursor.execute(""" CREATE TABLE IF NOT EXISTS device_metrics ( time TIMESTAMPTZ NOT NULL, cpu_percent FLOAT, memory_percent FLOAT ); """) # Convert to hypertable try: cursor.execute("SELECT create_hypertable('device_metrics', 'time', if_not_exists => TRUE);") print("Created device_metrics hypertable.") except psycopg2.Error as e: if "already a hypertable" in str(e): print("device_metrics is already a hypertable.") else: raise # Create GPU metrics table cursor.execute(""" CREATE TABLE IF NOT EXISTS gpu_metrics ( time TIMESTAMPTZ NOT NULL, memory_used_mb FLOAT, utilization FLOAT, temperature_c FLOAT ); """) # Convert to hypertable try: cursor.execute("SELECT create_hypertable('gpu_metrics', 'time', if_not_exists => TRUE);") print("Created gpu_metrics hypertable.") except psycopg2.Error as e: if "already a hypertable" in str(e): print("gpu_metrics is already a hypertable.") else: raise # Create detection metrics table cursor.execute(""" CREATE TABLE IF NOT EXISTS detection_metrics ( time TIMESTAMPTZ NOT NULL, model_name TEXT, confidence_score FLOAT, is_morphed BOOLEAN, processing_time_ms FLOAT, image_hash TEXT, face_count INTEGER, forensic_ticks INTEGER, reasoning_path TEXT, detection_tier TEXT, request_id TEXT ); """) # Convert to hypertable try: cursor.execute("SELECT create_hypertable('detection_metrics', 'time', if_not_exists => TRUE);") print("Created detection_metrics hypertable.") except psycopg2.Error as e: if "already a hypertable" in str(e): print("detection_metrics is already a hypertable.") else: raise # Create demorph metrics table cursor.execute(""" CREATE TABLE IF NOT EXISTS demorph_metrics ( time TIMESTAMPTZ NOT NULL, model_name TEXT, processing_time_ms FLOAT, original_image_hash TEXT, result_image_hash TEXT, method TEXT, success BOOLEAN, request_id TEXT ); """) # Convert to hypertable try: cursor.execute("SELECT create_hypertable('demorph_metrics', 'time', if_not_exists => TRUE);") print("Created demorph_metrics hypertable.") except psycopg2.Error as e: if "already a hypertable" in str(e): print("demorph_metrics is already a hypertable.") else: raise # Create liveness metrics table cursor.execute(""" CREATE TABLE IF NOT EXISTS liveness_metrics ( time TIMESTAMPTZ NOT NULL, is_live BOOLEAN, confidence FLOAT, attack_type TEXT, processing_time_ms FLOAT, image_hash TEXT, face_count INTEGER, request_id TEXT ); """) # Convert to hypertable try: cursor.execute("SELECT create_hypertable('liveness_metrics', 'time', if_not_exists => TRUE);") print("Created liveness_metrics hypertable.") except psycopg2.Error as e: if "already a hypertable" in str(e): print("liveness_metrics is already a hypertable.") else: raise # Create training metrics table cursor.execute(""" CREATE TABLE IF NOT EXISTS training_metrics ( time TIMESTAMPTZ NOT NULL, model_name TEXT, epoch INTEGER, loss FLOAT, accuracy FLOAT, val_loss FLOAT, val_accuracy FLOAT, learning_rate FLOAT, batch_size INTEGER, training_session_id TEXT ); """) # Convert to hypertable try: cursor.execute("SELECT create_hypertable('training_metrics', 'time', if_not_exists => TRUE);") print("Created training_metrics hypertable.") except psycopg2.Error as e: if "already a hypertable" in str(e): print("training_metrics is already a hypertable.") else: raise # Apply in-place migrations: ensure new columns exist for index creation print("Applying in-place migrations to add missing columns...") try: cursor.execute("ALTER TABLE detection_metrics ADD COLUMN IF NOT EXISTS model_name TEXT;") cursor.execute("ALTER TABLE detection_metrics ADD COLUMN IF NOT EXISTS forensic_ticks INTEGER;") cursor.execute("ALTER TABLE detection_metrics ADD COLUMN IF NOT EXISTS reasoning_path TEXT;") cursor.execute("ALTER TABLE detection_metrics ADD COLUMN IF NOT EXISTS detection_tier TEXT;") cursor.execute("ALTER TABLE demorph_metrics ADD COLUMN IF NOT EXISTS model_name TEXT;") cursor.execute("ALTER TABLE training_metrics ADD COLUMN IF NOT EXISTS model_name TEXT;") except psycopg2.Error as e: print(f"Warning: Could not apply migrations: {e}") # Create indexes for faster queries print("Creating indexes...") try: cursor.execute("CREATE INDEX IF NOT EXISTS idx_device_metrics_time ON device_metrics (time DESC);") cursor.execute("CREATE INDEX IF NOT EXISTS idx_gpu_metrics_time ON gpu_metrics (time DESC);") cursor.execute("CREATE INDEX IF NOT EXISTS idx_detection_metrics_time_model ON detection_metrics (time DESC, model_name);") cursor.execute("CREATE INDEX IF NOT EXISTS idx_detection_metrics_is_morphed ON detection_metrics (is_morphed, time DESC);") cursor.execute("CREATE INDEX IF NOT EXISTS idx_detection_metrics_request_id ON detection_metrics (request_id);") cursor.execute("CREATE INDEX IF NOT EXISTS idx_demorph_metrics_time_model ON demorph_metrics (time DESC, model_name);") cursor.execute("CREATE INDEX IF NOT EXISTS idx_demorph_metrics_method ON demorph_metrics (method, time DESC);") cursor.execute("CREATE INDEX IF NOT EXISTS idx_demorph_metrics_request_id ON demorph_metrics (request_id);") cursor.execute("CREATE INDEX IF NOT EXISTS idx_liveness_metrics_time ON liveness_metrics (time DESC);") cursor.execute("CREATE INDEX IF NOT EXISTS idx_liveness_metrics_is_live ON liveness_metrics (is_live, time DESC);") cursor.execute("CREATE INDEX IF NOT EXISTS idx_liveness_metrics_attack_type ON liveness_metrics (attack_type);") cursor.execute("CREATE INDEX IF NOT EXISTS idx_training_metrics_model_time ON training_metrics (model_name, time DESC);") cursor.execute("CREATE INDEX IF NOT EXISTS idx_training_metrics_session_epoch ON training_metrics (training_session_id, epoch);") except psycopg2.Error as e: print(f"Warning: Could not create indexes: {e}") # Create retention policies (keep data for 30/90 days) print("Setting up retention policies...") try: # System metrics - retain for 30 days cursor.execute("SELECT add_retention_policy('device_metrics', INTERVAL '30 days', if_not_exists => TRUE);") cursor.execute("SELECT add_retention_policy('gpu_metrics', INTERVAL '30 days', if_not_exists => TRUE);") # Model metrics - retain for 90 days cursor.execute("SELECT add_retention_policy('detection_metrics', INTERVAL '90 days', if_not_exists => TRUE);") cursor.execute("SELECT add_retention_policy('demorph_metrics', INTERVAL '90 days', if_not_exists => TRUE);") cursor.execute("SELECT add_retention_policy('liveness_metrics', INTERVAL '90 days', if_not_exists => TRUE);") # Training metrics - retain for 365 days (keep longer for historical analysis) cursor.execute("SELECT add_retention_policy('training_metrics', INTERVAL '365 days', if_not_exists => TRUE);") # Cloud connector metrics - retain for 90 days cursor.execute("SELECT add_retention_policy('cloud_connector_metrics', INTERVAL '90 days', if_not_exists => TRUE);") # Verification events - retain for 1 year (important for audit trail) cursor.execute("SELECT add_retention_policy('verification_events', INTERVAL '365 days', if_not_exists => TRUE);") except psycopg2.Error as e: print(f"Warning: Could not set retention policies: {e}") print("You may need to manually configure retention policies or upgrade TimescaleDB.") # Create model interactions table for AI model mapping print("Creating model_interactions table...") cursor.execute(""" CREATE TABLE IF NOT EXISTS model_interactions ( time TIMESTAMPTZ NOT NULL, source_model TEXT NOT NULL, dest_model TEXT NOT NULL, value BIGINT NOT NULL DEFAULT 1, details JSONB ); """) # Convert to hypertable try: cursor.execute("SELECT create_hypertable('model_interactions', 'time', if_not_exists => TRUE);") print("Created model_interactions hypertable.") except psycopg2.Error as e: if "already a hypertable" in str(e): print("model_interactions is already a hypertable.") else: raise # Create indexes cursor.execute("CREATE INDEX IF NOT EXISTS idx_model_interactions_time ON model_interactions (time DESC);") cursor.execute("CREATE INDEX IF NOT EXISTS idx_model_interactions_source_dest ON model_interactions (source_model, dest_model);") # Create cloud connector metrics table print("Creating cloud_connector_metrics table...") cursor.execute(""" CREATE TABLE IF NOT EXISTS cloud_connector_metrics ( time TIMESTAMPTZ NOT NULL, provider TEXT NOT NULL, operation TEXT NOT NULL, success BOOLEAN NOT NULL, response_time_ms FLOAT, cost_estimate FLOAT, api_calls_used INTEGER, free_tier_remaining INTEGER, error_message TEXT, request_id TEXT, confidence_score FLOAT, details JSONB ); """) # Convert to hypertable try: cursor.execute("SELECT create_hypertable('cloud_connector_metrics', 'time', if_not_exists => TRUE);") print("Created cloud_connector_metrics hypertable.") except psycopg2.Error as e: if "already a hypertable" in str(e): print("cloud_connector_metrics is already a hypertable.") else: raise # Create users table (migrating from SQLite) print("Creating users table...") cursor.execute(""" CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'user', email TEXT, created_at TIMESTAMPTZ DEFAULT NOW(), last_login TIMESTAMPTZ, active BOOLEAN DEFAULT TRUE ); """) cursor.execute("CREATE INDEX IF NOT EXISTS idx_users_username ON users (username);") # Create API Keys table create_api_keys_table(cursor) # Create indexes for cloud metrics cursor.execute("CREATE INDEX IF NOT EXISTS idx_cloud_metrics_time ON cloud_connector_metrics (time DESC);") cursor.execute("CREATE INDEX IF NOT EXISTS idx_cloud_metrics_provider ON cloud_connector_metrics (provider, time DESC);") cursor.execute("CREATE INDEX IF NOT EXISTS idx_cloud_metrics_operation ON cloud_connector_metrics (operation, time DESC);") cursor.execute("CREATE INDEX IF NOT EXISTS idx_cloud_metrics_success ON cloud_connector_metrics (success, time DESC);") cursor.execute("CREATE INDEX IF NOT EXISTS idx_cloud_metrics_request_id ON cloud_connector_metrics (request_id);") # Create verification events table for blockchain logging print("Creating verification_events table...") cursor.execute(""" CREATE TABLE IF NOT EXISTS verification_events ( time TIMESTAMPTZ NOT NULL, verification_type TEXT NOT NULL, subject_hash TEXT NOT NULL, result TEXT NOT NULL, confidence FLOAT, provider TEXT, blockchain_tx_hash TEXT, verification_id TEXT, metadata JSONB ); """) # Convert to hypertable try: cursor.execute("SELECT create_hypertable('verification_events', 'time', if_not_exists => TRUE);") print("Created verification_events hypertable.") except psycopg2.Error as e: if "already a hypertable" in str(e): print("verification_events is already a hypertable.") else: raise # Create indexes for verification events cursor.execute("CREATE INDEX IF NOT EXISTS idx_verification_events_time ON verification_events (time DESC);") cursor.execute("CREATE INDEX IF NOT EXISTS idx_verification_events_type ON verification_events (verification_type, time DESC);") cursor.execute("CREATE INDEX IF NOT EXISTS idx_verification_events_result ON verification_events (result, time DESC);") cursor.execute("CREATE INDEX IF NOT EXISTS idx_verification_events_blockchain ON verification_events (blockchain_tx_hash);") cursor.execute("CREATE INDEX IF NOT EXISTS idx_verification_events_verification_id ON verification_events (verification_id);") # Create inference metrics table for logging inference time print("Creating inference_metrics table...") cursor.execute(""" CREATE TABLE IF NOT EXISTS inference_metrics ( time TIMESTAMPTZ NOT NULL, model_name TEXT, duration_ms FLOAT, confidence FLOAT, face_count INTEGER, request_id TEXT ); """) try: cursor.execute("SELECT create_hypertable('inference_metrics', 'time', if_not_exists => TRUE);") print("Created inference_metrics hypertable.") except psycopg2.Error as e: if "already a hypertable" in str(e): print("inference_metrics is already a hypertable.") else: raise # Create a unified 'metrics' view for Grafana dashboards print("Creating unified metrics view...") try: cursor.execute(""" CREATE OR REPLACE VIEW metrics AS -- Training metrics: accuracy and loss SELECT time, 'accuracy' AS metric_name, accuracy AS value, CASE WHEN model_name = 'morph_detector' THEN 'detector' WHEN model_name = 'demorpher' THEN 'demorpher' ELSE model_name END AS model_type FROM training_metrics UNION ALL SELECT time, 'loss', loss, CASE WHEN model_name = 'morph_detector' THEN 'detector' WHEN model_name = 'demorpher' THEN 'demorpher' ELSE model_name END FROM training_metrics UNION ALL -- GPU utilization SELECT time, 'gpu_usage', utilization AS value, 'system' AS model_type FROM gpu_metrics UNION ALL -- Inference time SELECT time, 'inference_time', duration_ms AS value, CASE WHEN model_name = 'morph_detector' THEN 'detector' ELSE model_name END FROM inference_metrics UNION ALL -- Cloud connector response times SELECT time, 'cloud_response_time', response_time_ms AS value, provider AS model_type FROM cloud_connector_metrics WHERE response_time_ms IS NOT NULL UNION ALL -- Cloud API usage SELECT time, 'cloud_api_calls', api_calls_used AS value, provider AS model_type FROM cloud_connector_metrics WHERE api_calls_used IS NOT NULL UNION ALL -- Cloud success rate SELECT time, 'cloud_success_rate', CASE WHEN success THEN 1.0 ELSE 0.0 END AS value, provider AS model_type FROM cloud_connector_metrics; """) except Exception as e: print(f"Warning: could not create unified metrics view: {e}") conn.commit() print("Database setup completed (schema version deployed).") # Record completion # Update schema version cursor.execute("INSERT INTO schema_version (version) VALUES (%s);", (TARGET_SCHEMA_VERSION,)) print(f"Database setup completed (schema version {TARGET_SCHEMA_VERSION}).") except Exception as e: print(f"Error: {e}") if conn: conn.rollback() finally: if conn: conn.close() def main(): # Try to import config.py for default database settings try: sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) import config default_host = getattr(config, 'DB_HOST', 'localhost') default_port = getattr(config, 'DB_PORT', 5432) default_dbname = getattr(config, 'DB_NAME', 'morphguard') default_user = getattr(config, 'DB_USER', 'morphguard') default_password = getattr(config, 'DB_PASSWORD', 'morphguard') except ImportError: # Use defaults if config.py not found default_host = 'localhost' default_port = 5432 default_dbname = 'morphguard' default_user = 'morphguard' default_password = 'morphguard' # Parse command-line arguments parser = argparse.ArgumentParser(description='Set up TimescaleDB tables for MorphGuard') parser.add_argument('--host', default=default_host, help=f'Database host (default: {default_host})') parser.add_argument('--port', type=int, default=default_port, help=f'Database port (default: {default_port})') parser.add_argument('--dbname', default=default_dbname, help=f'Database name (default: {default_dbname})') parser.add_argument('--user', default=default_user, help=f'Database user (default: {default_user})') parser.add_argument('--password', default=default_password, help=f'Database password (default: {default_password})') args = parser.parse_args() # Create tables create_metrics_tables( host=args.host, port=args.port, dbname=args.dbname, user=args.user, password=args.password ) def initialize_schema(): """ Initialize TimescaleDB schema programmatically using default settings from config.py. """ try: import config host = getattr(config, 'DB_HOST', 'localhost') port = getattr(config, 'DB_PORT', 5432) dbname = getattr(config, 'DB_NAME', 'morphguard') user = getattr(config, 'DB_USER', 'morphguard') password = getattr(config, 'DB_PASSWORD', 'morphguard') except ImportError: host = 'localhost' port = 5432 dbname = 'morphguard' user = 'morphguard' password = 'morphguard' create_metrics_tables(host=host, port=port, dbname=dbname, user=user, password=password) if __name__ == "__main__": main()