Răsfoiți Sursa

TG-196: Full security refactor, Taiga sync, and Data pipeline automation

lanfr144 1 lună în urmă
părinte
comite
ade82aff87
11 a modificat fișierele cu 68 adăugiri și 502 ștergeri
  1. BIN
      .app.py.swp
  2. 3 4
      app.py
  3. 7 2
      backup_db.sh
  4. 0 0
      configure_zabbix_email.py
  5. 56 25
      data_sync.sh
  6. 0 125
      docker-compose.yml
  7. 0 23
      docs/architecture.md
  8. 0 186
      init.sql
  9. 2 0
      k8s/secret.yaml.example
  10. 0 85
      rotate_passwords.py
  11. 0 52
      zabbix_telemetry.py

BIN
.app.py.swp


+ 3 - 4
app.py

@@ -143,7 +143,7 @@ def verify_login(username, password):
     conn = get_db_connection('app_auth')
     if not conn: return False
     with conn.cursor() as cursor:
-        cursor.execute("SELECT password_hash FROM users WHERE username = %s LIMIT 1", (username,))
+        cursor.execute("SELECT password_hash FROM users WHERE username = %s", (username,))
         result = cursor.fetchone()
         conn.close()
         if result: return bcrypt.checkpw(password.encode('utf-8'), result['password_hash'].encode('utf-8'))
@@ -153,7 +153,7 @@ def get_user_id(username):
     conn = get_db_connection('app_auth')
     if not conn: return None
     with conn.cursor() as cursor:
-        cursor.execute("SELECT id FROM users WHERE username = %s LIMIT 1", (username,))
+        cursor.execute("SELECT id FROM users WHERE username = %s", (username,))
         result = cursor.fetchone()
         conn.close()
         return result['id'] if result else None
@@ -172,7 +172,7 @@ def get_user_limit(username):
     conn = get_db_connection('app_auth')
     if not conn: return "50"
     with conn.cursor() as cursor:
-        cursor.execute("SELECT search_limit FROM users WHERE username = %s LIMIT 1", (username,))
+        cursor.execute("SELECT search_limit FROM users WHERE username = %s", (username,))
         result = cursor.fetchone()
         conn.close()
         return result['search_limit'] if (result and result['search_limit']) else "50"
@@ -751,7 +751,6 @@ with tab_plate:
                             WHERE m.proteins_100g IS NOT NULL AND m.fat_100g IS NOT NULL AND m.carbohydrates_100g IS NOT NULL
                             {wh_comp}
                             {order_by}
-                            LIMIT 15
                         """
                         cursor.execute(sql, (bool_search,))
                         return cursor.fetchall()

+ 7 - 2
backup_db.sh

@@ -17,8 +17,13 @@ echo "Starting Database Backup for $DB_NAME..."
 # Ensure backup directory exists
 mkdir -p "$BACKUP_DIR"
 
-# Execute mysqldump inside the container and pipe to gzip
-sudo docker exec $DB_CONTAINER mysqldump -u root -proot_pass $DB_NAME | gzip > "$BACKUP_FILE"
+# Load credentials from configuration file
+if [ -f "./.env" ]; then
+    source ./.env
+fi
+
+# Execute mysqldump inside the container securely via environment variable
+sudo docker exec -e MYSQL_PWD="${MYSQL_ROOT_PASSWORD}" $DB_CONTAINER mysqldump -u root $DB_NAME | gzip > "$BACKUP_FILE"
 
 if [ $? -eq 0 ]; then
     echo "Backup successfully saved to $BACKUP_FILE"

+ 0 - 0
configure_zabbix_email.py


+ 56 - 25
data_sync.sh

@@ -1,29 +1,25 @@
 #!/bin/bash
-# $Id$
-# $Author$
-# $log$
-#ident "@(#)LocalFoodAI:data_sync.sh:$Format:%D:%ci:%cN:%h$"
 # data_sync.sh - Automated Data Freshness Pipeline
 
+LOG_DIR="./logs"
+mkdir -p "$LOG_DIR"
+LOG_FILE="$LOG_DIR/data_sync.log"
+
 # --- Auto-Detach & Sudo Auth Block ---
 if [ "$1" = "--detached" ]; then
     shift # Remove --detached from arguments for normal parsing
 else
     echo "Preparing to run ingestion in the background to survive SSH disconnections."
-    echo "Please provide your sudo password to authorize the background task:"
-    sudo -v # Authenticate interactively upfront
-    if [ $? -ne 0 ]; then
-        echo "Authentication failed. Exiting."
-        exit 1
-    fi
-    echo "Authentication successful! Detaching process..."
-    nohup sudo "$0" --detached "$@" > data_sync.log 2>&1 < /dev/null &
+    nohup sudo "$0" --detached "$@" > "$LOG_FILE" 2>&1 < /dev/null &
     echo "Process successfully detached! You can now safely close your SSH connection."
-    echo "To monitor progress at any time, type: tail -f data_sync.log"
+    echo "To monitor progress at any time, type: tail -f $LOG_FILE"
     exit 0
 fi
 # -------------------------------------
 
+if [ -f "./.env" ]; then
+    source ./.env
+fi
 
 ONLINE_MODE=0
 DATA_DIR="./data"
@@ -39,17 +35,17 @@ while [[ "$#" -gt 0 ]]; do
     shift
 done
 
-echo "Starting Data Freshness Sync..."
+echo "Starting Data Freshness Sync at $(date)..."
 mkdir -p "$DATA_DIR"
 
 if [ "$ONLINE_MODE" -eq 1 ]; then
     echo "Online mode enabled. Checking for latest OpenFoodFacts database..."
     if ping -c 1 google.com &> /dev/null; then
         echo "Internet connection verified. Downloading latest dataset..."
-        # Use -N to only download if newer than local file
-        wget -N -P "$DATA_DIR" "$URL"
+        # Download strictly over HTTP to avoid certificate issues on some embedded devices
+        wget -q -N -P "$DATA_DIR" "$URL"
         if [ $? -eq 0 ]; then
-            echo "Download complete."
+            echo "Download check complete."
         else
             echo "Failed to download dataset."
         fi
@@ -62,16 +58,51 @@ fi
 
 # Check if file exists to trigger ingestion
 if [ -f "$DATA_DIR/$INGEST_FILE" ]; then
-    # We should only ingest if the file is new or modified. 
-    # For simplicity, we just trigger ingestion if the file exists.
-    # Ingest script handles DROP TABLE if needed, but wait: ingest_csv appends by default or we can modify it.
     echo "Found dataset: $DATA_DIR/$INGEST_FILE"
-    echo "Triggering ingestion pipeline via Docker Compose..."
-    sudo docker-compose run --rm ingest ./ingest_csv.py "data/$INGEST_FILE"
     
-    # After successful ingestion, move or rename to prevent infinite loops on offline cron
-    mv "$DATA_DIR/$INGEST_FILE" "$DATA_DIR/$INGEST_FILE.processed"
-    echo "Ingestion complete and file archived."
+    SHOULD_INGEST=0
+    
+    # 1. Checksum Validation
+    NEW_CHECKSUM=$(md5sum "$DATA_DIR/$INGEST_FILE" | awk '{ print $1 }')
+    OLD_CHECKSUM=""
+    if [ -f "$DATA_DIR/checksum.md5" ]; then
+        OLD_CHECKSUM=$(cat "$DATA_DIR/checksum.md5")
+    fi
+    
+    if [ "$NEW_CHECKSUM" != "$OLD_CHECKSUM" ]; then
+        echo "Checksum mismatch: File is new or modified. Ingestion required."
+        SHOULD_INGEST=1
+    else
+        echo "Checksum matches previously processed file."
+    fi
+    
+    # 2. Database Row Count Validation
+    DB_COUNT=$(sudo docker exec -e MYSQL_PWD="${MYSQL_ROOT_PASSWORD}" food_project-mysql-1 mysql -u root -N -B -e "SELECT COUNT(*) FROM food_db.products_core;" 2>/dev/null)
+    CSV_COUNT=$(wc -l < "$DATA_DIR/$INGEST_FILE")
+    CSV_COUNT=$((CSV_COUNT - 1)) # Ignore header
+    
+    if [ -z "$DB_COUNT" ]; then
+        DB_COUNT=0
+    fi
+    
+    echo "Rows in DB: $DB_COUNT | Rows in CSV: $CSV_COUNT"
+    if [ "$DB_COUNT" -lt "$CSV_COUNT" ]; then
+        echo "Database is missing rows. Ingestion required."
+        SHOULD_INGEST=1
+    fi
+    
+    if [ "$SHOULD_INGEST" -eq 1 ]; then
+        echo "Triggering ingestion pipeline via Docker Compose..."
+        sudo docker-compose run --rm ingest ./ingest_csv.py "data/$INGEST_FILE"
+        if [ $? -eq 0 ]; then
+            echo "$NEW_CHECKSUM" > "$DATA_DIR/checksum.md5"
+            echo "Ingestion complete and checksum saved."
+        else
+            echo "Error: Ingestion failed!"
+        fi
+    else
+        echo "Database is fully synchronized. Skipping ingestion."
+    fi
 else
     echo "No dataset found in $DATA_DIR. Nothing to ingest."
 fi

+ 0 - 125
docker-compose.yml

@@ -1,125 +0,0 @@
-
-services:
-  mysql:
-    build:
-      context: ./docker/mysql
-    ports:
-      - "3307:3306"
-    volumes:
-      - mysql_data:/var/lib/mysql
-      - ./my.cnf:/etc/mysql/conf.d/custom_ai_app.cnf
-      - ./init.sql:/docker-entrypoint-initdb.d/1-init.sql
-      - ./init_zabbix_db.sh:/docker-entrypoint-initdb.d/2-init_zabbix.sh
-    environment:
-      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
-    healthcheck:
-      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
-      interval: 10s
-      timeout: 5s
-      retries: 20
-    restart: always
-
-  ollama:
-    image: ollama/ollama:latest
-    volumes:
-      - ollama_data:/root/.ollama
-    restart: always
-
-  searxng:
-    image: searxng/searxng:latest
-    ports:
-      - "8080:8080"
-    volumes:
-      - ./searxng:/etc/searxng
-    environment:
-      - SEARXNG_BASE_URL=http://localhost:8080/
-    restart: always
-
-  app:
-    build:
-      context: .
-      dockerfile: docker/app/Dockerfile
-    ports:
-      - "8502:8501"
-    environment:
-      - DB_HOST=mysql
-      - DB_USER=db_reader
-      - DB_PASS=${DB_READER_PASS}
-      - APP_AUTH_USER=db_app_auth
-      - APP_AUTH_PASS=${DB_APP_AUTH_PASS}
-      - OLLAMA_HOST=http://ollama:11434
-      - SEARXNG_HOST=http://searxng:8080
-    depends_on:
-      mysql:
-        condition: service_healthy
-    restart: always
-
-  ingest:
-    build:
-      context: .
-      dockerfile: docker/ingest/Dockerfile
-    environment:
-      - DB_HOST=mysql
-      - DB_USER=db_loader
-      - DB_PASS=${DB_LOADER_PASS}
-    volumes:
-      - ./:/app
-    depends_on:
-      mysql:
-        condition: service_healthy
-    profiles:
-      - manual # Only runs when explicitly requested
-
-  zabbix-server:
-    image: zabbix/zabbix-server-mysql:ubuntu-7.0-latest
-    environment:
-      - DB_SERVER_HOST=mysql
-      - MYSQL_USER=zabbix
-      - MYSQL_PASSWORD=${MYSQL_ZABBIX_PASSWORD}
-      - ZBX_SNMPTRAPPER=1
-    depends_on:
-      mysql:
-        condition: service_healthy
-    restart: always
-
-  zabbix-web:
-    image: zabbix/zabbix-web-nginx-mysql:ubuntu-7.0-latest
-    ports:
-      - "8081:8080"
-      - "8444:8443"
-    environment:
-      - DB_SERVER_HOST=mysql
-      - MYSQL_USER=zabbix
-      - MYSQL_PASSWORD=${MYSQL_ZABBIX_PASSWORD}
-      - ZBX_SERVER_HOST=zabbix-server
-      - PHP_TZ=Europe/Paris
-    depends_on:
-      - zabbix-server
-    restart: always
-
-  zabbix-agent:
-    image: zabbix/zabbix-agent:ubuntu-7.0-latest
-    environment:
-      - ZBX_HOSTNAME=Zabbix server
-      - ZBX_SERVER_HOST=zabbix-server
-    privileged: true
-    pid: "host"
-    volumes:
-      - /var/run:/var/run
-    depends_on:
-      - zabbix-server
-    restart: always
-
-  nginx:
-    image: nginx:latest
-    ports:
-      - "80:80"
-    volumes:
-      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
-    depends_on:
-      - app
-    restart: always
-
-volumes:
-  mysql_data:
-  ollama_data:

+ 0 - 23
docs/architecture.md

@@ -1,23 +0,0 @@
-# Local Food AI: Architecture Map
-
-## 1. Core Stack
-- **Database**: MySQL 8.0 (Partitioned for 3GB+ OpenFoodFacts dataset).
-- **Backend & Frontend**: Python 3.11 with Streamlit.
-- **AI Engine**: Ollama running locally with `llama3.2:1b` (quantized for 30GB RAM limits).
-- **Web Search**: SearXNG Private Engine (used dynamically when the local DB lacks specific food heuristics).
-- **Monitoring**: Zabbix Telemetry Server (connected via native Python SNMP traps and container-level SNMP daemons).
-
-## 2. Security Infrastructure
-- **Zero Cloud Policy**: 100% of the AI processing, Database searching, and Telemetry happens locally on the Ubuntu VM. No user dietary queries leave the machine.
-- **Principle of Least Privilege (PoLP)**:
-  - `db_app_auth`: Only has access to the authentication tables.
-  - `db_reader`: Only has `SELECT` privileges on the food partitions.
-  - `db_loader`: Only has `INSERT` privileges for the background CSV script.
-- **Encryption**: User passwords are mathematically salted and hashed using `bcrypt` (Blowfish cipher).
-
-## 3. Distributed Microservice Networking
-This stack is designed to be highly decoupled. While typically run via a unified `docker-compose.yml`, the application supports distributed routing across:
-1. WSL2 Nodes (Frontend App)
-2. Hyper-V Instances (MySQL Partition Clusters)
-3. VirtualBox Hosts (Ollama GPU/CPU compute nodes)
-*(Refer to `distributed_deployment.md` for specific Bridged Adapter setups).*

+ 0 - 186
init.sql

@@ -1,186 +0,0 @@
--- ---------------------------------------------------------
--- Initial Database and User Setup (Run as MySQL Root)
--- ---------------------------------------------------------
-CREATE DATABASE IF NOT EXISTS food_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-
--- 1. Create the Owner User
--- Has full rights and can grant privileges to others.
-CREATE USER IF NOT EXISTS 'db_owner'@'%' IDENTIFIED BY 'owner_pass';
-GRANT ALL PRIVILEGES ON food_db.* TO 'db_owner'@'%' WITH GRANT OPTION;
-
--- 2. Create the Reader User
--- Has only connect and read permissions.
-CREATE USER IF NOT EXISTS 'db_reader'@'%' IDENTIFIED BY 'reader_pass';
-GRANT USAGE ON *.* TO 'db_reader'@'%';
-
--- 3. Create the Loader User
--- Has connect and data manipulation permissions to load files.
-CREATE USER IF NOT EXISTS 'db_loader'@'%' IDENTIFIED BY 'loader_pass';
-GRANT USAGE ON *.* TO 'db_loader'@'%';
-GRANT FILE ON *.* TO 'db_loader'@'%'; -- Essential for LOAD DATA INFILE from any directory
-
--- 4. Create the App Auth User
--- Segregation of Duties: Handles only users table for web application routing.
-CREATE USER IF NOT EXISTS 'db_app_auth'@'%' IDENTIFIED BY 'app_auth_placeholder_pass';
--- Note: Replace 'app_auth_placeholder_pass' later outside this script.
-GRANT USAGE ON *.* TO 'db_app_auth'@'%';
-
-FLUSH PRIVILEGES;
-
-
--- ---------------------------------------------------------
--- Table Creation & Grants (Logically executed by db_owner)
--- ---------------------------------------------------------
-USE food_db;
-
--- NOTE: The syntax you provided (`read_csv_auto`) is specific to DuckDB!
--- MySQL does NOT support `read_csv_auto()` to dynamically create tables from CSV.
--- In MySQL, you MUST define the table schema first, and then use LOAD DATA INFILE.
--- Here is the MySQL equivalent process:
-
--- Step A.1: Create Web Users Table
-CREATE TABLE IF NOT EXISTS users (
-    id INT AUTO_INCREMENT PRIMARY KEY,
-    username VARCHAR(100) UNIQUE NOT NULL,
-    password_hash VARCHAR(255) NOT NULL,
-    email VARCHAR(255),
-    search_limit VARCHAR(50) DEFAULT '50',
-    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
-) ENGINE=InnoDB;
-
-GRANT SELECT, INSERT, UPDATE ON food_db.users TO 'db_app_auth'@'%';
-
--- Step A.2: Create Health Profiles Table
-CREATE TABLE IF NOT EXISTS user_health_profiles (
-    id INT AUTO_INCREMENT PRIMARY KEY,
-    user_id INT NOT NULL,
-    illness_health_condition_diet_dislikes_name VARCHAR(100),
-    illness_health_condition_diet_dislikes_value VARCHAR(255),
-    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
-) ENGINE=InnoDB;
-
-GRANT SELECT, INSERT, UPDATE, DELETE ON food_db.user_health_profiles TO 'db_app_auth'@'%';
-
--- Step A.3: Create Plate Builder Tables
-CREATE TABLE IF NOT EXISTS plates (
-    id INT AUTO_INCREMENT PRIMARY KEY,
-    user_id INT NOT NULL,
-    plate_name VARCHAR(255) NOT NULL,
-    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
-) ENGINE=InnoDB;
-
-CREATE TABLE IF NOT EXISTS plate_items (
-    id INT AUTO_INCREMENT PRIMARY KEY,
-    plate_id INT NOT NULL,
-    product_code VARCHAR(50) NOT NULL,
-    quantity_grams FLOAT NOT NULL,
-    FOREIGN KEY (plate_id) REFERENCES plates(id) ON DELETE CASCADE
-) ENGINE=InnoDB;
-
-GRANT SELECT, INSERT, UPDATE, DELETE ON food_db.plates TO 'db_app_auth'@'%';
-GRANT SELECT, INSERT, UPDATE, DELETE ON food_db.plate_items TO 'db_app_auth'@'%';
-FLUSH PRIVILEGES;
-
--- Step A.2: Create the table with known columns (Example structure for OpenFoodFacts)
-CREATE TABLE IF NOT EXISTS products (
-    code VARCHAR(50) PRIMARY KEY,
-    url TEXT,
-    creator VARCHAR(255),
-    created_t VARCHAR(50),
-    created_datetime VARCHAR(50),
-    last_modified_t VARCHAR(50),
-    last_modified_datetime VARCHAR(50),
-    product_name TEXT,
-    generic_name TEXT,
-    quantity VARCHAR(255),
-    packaging TEXT,
-    brands TEXT,
-    categories TEXT,
-    origins TEXT,
-    labels TEXT,
-    stores TEXT,
-    countries TEXT,
-    ingredients_text TEXT,
-    allergens TEXT,
-    traces TEXT,
-    
-    -- Add FULLTEXT index for context search on ingredients and products
-    FULLTEXT INDEX ft_idx_search (product_name, ingredients_text)
-) ENGINE=InnoDB;
-
-CREATE TABLE IF NOT EXISTS products_core (
-    code VARCHAR(50) PRIMARY KEY,
-    product_name TEXT,
-    generic_name TEXT,
-    brands TEXT,
-    ingredients_text TEXT,
-    FULLTEXT INDEX ft_idx_search (product_name, ingredients_text),
-    FULLTEXT INDEX ft_idx_pn (product_name),
-    FULLTEXT INDEX ft_idx_it (ingredients_text)
-) ENGINE=InnoDB;
-
-CREATE TABLE IF NOT EXISTS products_macros (
-    code VARCHAR(50) PRIMARY KEY,
-    `energy-kcal_100g` DOUBLE,
-    proteins_100g DOUBLE,
-    fat_100g DOUBLE,
-    carbohydrates_100g DOUBLE,
-    sugars_100g DOUBLE,
-    fiber_100g DOUBLE,
-    sodium_100g DOUBLE,
-    salt_100g DOUBLE,
-    cholesterol_100g DOUBLE
-) ENGINE=InnoDB;
-
-CREATE TABLE IF NOT EXISTS products_vitamins (
-    code VARCHAR(50) PRIMARY KEY,
-    `vitamin-a_100g` DOUBLE,
-    `vitamin-b1_100g` DOUBLE,
-    `vitamin-b2_100g` DOUBLE,
-    `vitamin-pp_100g` DOUBLE,
-    `vitamin-b6_100g` DOUBLE,
-    `vitamin-b9_100g` DOUBLE,
-    `vitamin-b12_100g` DOUBLE,
-    `vitamin-c_100g` DOUBLE,
-    `vitamin-d_100g` DOUBLE,
-    `vitamin-e_100g` DOUBLE,
-    `vitamin-k_100g` DOUBLE
-) ENGINE=InnoDB;
-
-CREATE TABLE IF NOT EXISTS products_minerals (
-    code VARCHAR(50) PRIMARY KEY,
-    calcium_100g DOUBLE,
-    iron_100g DOUBLE,
-    magnesium_100g DOUBLE,
-    potassium_100g DOUBLE,
-    zinc_100g DOUBLE
-) ENGINE=InnoDB;
-
-CREATE TABLE IF NOT EXISTS products_allergens (
-    code VARCHAR(50) PRIMARY KEY,
-    allergens TEXT,
-    traces TEXT
-) ENGINE=InnoDB;
-
--- Step B: The Owner grants explicit privileges to the Reader and Loader
--- Grant explicit privileges to the Reader
-GRANT SELECT ON food_db.products TO 'db_reader'@'%';
-GRANT SELECT ON food_db.products_core TO 'db_reader'@'%';
-GRANT SELECT ON food_db.products_allergens TO 'db_reader'@'%';
-GRANT SELECT ON food_db.products_macros TO 'db_reader'@'%';
-GRANT SELECT ON food_db.products_vitamins TO 'db_reader'@'%';
-GRANT SELECT ON food_db.products_minerals TO 'db_reader'@'%';
-
--- Grant broad privileges to the Loader on food_db to allow temp tables and UPSERT modifications
-GRANT SELECT, INSERT, UPDATE, DELETE, DROP, CREATE, ALTER, INDEX ON food_db.* TO 'db_loader'@'%';
-FLUSH PRIVILEGES;
-
--- Step C: The Loader user would then run this MySQL command to import:
-/*
-LOAD DATA INFILE '/path/to/en.openfoodfacts.org.products.converted.csv'
-INTO TABLE products
-FIELDS TERMINATED BY '\t'
-ENCLOSED BY ''
-LINES TERMINATED BY '\n'
-IGNORE 1 ROWS;
-*/

+ 2 - 0
k8s/secret.yaml.example

@@ -14,3 +14,5 @@ stringData:
   EMAIL_USER: "your_email@example.com"
 
   EMAIL_PASS: "placeholder_email_pass"
+  TAIGA_USER: base64_encoded_placeholder
+  TAIGA_PASS: base64_encoded_placeholder

+ 0 - 85
rotate_passwords.py

@@ -1,85 +0,0 @@
-import pymysql
-import os
-import secrets
-import string
-import subprocess
-from dotenv import load_dotenv
-
-def generate_password(length=16):
-    characters = string.ascii_letters + string.digits + "!@#$%^&*"
-    return ''.join(secrets.choice(characters) for _ in range(length))
-
-def update_env_file(passwords):
-    env_file = '.env'
-    lines = []
-    if os.path.exists(env_file):
-        with open(env_file, 'r') as f:
-            lines = f.readlines()
-            
-    # Remove old password lines
-    lines = [l for l in lines if not any(l.startswith(f"{k}=") for k in passwords.keys())]
-    
-    # Add new passwords
-    for key, val in passwords.items():
-        lines.append(f"{key}={val}\n")
-        
-    with open(env_file, 'w') as f:
-        f.writelines(lines)
-    print("✅ .env file updated with new synchronized passwords.")
-
-def main():
-    load_dotenv()
-    print("🔄 Starting Password Synchronization Routine...")
-    
-    # 1. Connect to MySQL as root
-    try:
-        conn = pymysql.connect(
-            host='192.168.130.170',  # Assuming we run this from host to mapped port, or within a container network
-            port=3307,
-            user='root',
-            password=os.environ.get('MYSQL_ROOT_PASSWORD', 'root_pass'),
-            database='food_db'
-        )
-    except Exception as e:
-        print(f"❌ Could not connect to MySQL: {e}")
-        return
-
-    # 2. Generate new passwords
-    new_passwords = {
-        'DB_READER_PASS': generate_password(),
-        'DB_LOADER_PASS': generate_password(),
-        'DB_APP_AUTH_PASS': generate_password()
-    }
-    
-    # 3. Update MySQL Users
-    try:
-        with conn.cursor() as cursor:
-            cursor.execute("ALTER USER 'db_reader'@'%%' IDENTIFIED BY %s", (new_passwords['DB_READER_PASS'],))
-            cursor.execute("ALTER USER 'db_loader'@'%%' IDENTIFIED BY %s", (new_passwords['DB_LOADER_PASS'],))
-            cursor.execute("ALTER USER 'db_app_auth'@'%%' IDENTIFIED BY %s", (new_passwords['DB_APP_AUTH_PASS'],))
-            cursor.execute("FLUSH PRIVILEGES")
-            conn.commit()
-        print("✅ Database user passwords rotated successfully.")
-    except Exception as e:
-        print(f"❌ Failed to alter database users: {e}")
-    finally:
-        conn.close()
-        
-    # 4. Update .env file so Docker Compose picks it up
-    update_env_file(new_passwords)
-    
-    # 5. Gracefully restart client containers to sync connection
-    print("🔄 Restarting App and Ingest containers to synchronize new credentials...")
-    try:
-        # Pass a clean environment without cached variables so docker-compose reads the updated .env file
-        env = os.environ.copy()
-        for k in new_passwords.keys():
-            env.pop(k, None)
-        subprocess.run(["docker-compose", "up", "-d", "app"], check=True, env=env)
-        # We don't necessarily need to restart ingest if it's manual, but we can recreate it if it was running.
-        print("✅ Client containers synchronized with new database passwords!")
-    except Exception as e:
-        print(f"⚠️ Failed to restart docker containers: {e}")
-
-if __name__ == "__main__":
-    main()

+ 0 - 52
zabbix_telemetry.py

@@ -1,52 +0,0 @@
-import os
-import time
-import pymysql
-from snmp_notifier import notifier
-
-def get_db_connection():
-    try:
-        # Defaults to host environment or localhost for external execution
-        db_host = os.environ.get('DB_HOST', '127.0.0.1')
-        db_user = os.environ.get('DB_READER_USER', 'db_reader')
-        db_pass = os.environ.get('DB_READER_PASS', 'reader_pass')
-
-        return pymysql.connect(
-            host=db_host,
-            user=db_user,
-            password=db_pass,
-            database='food_db',
-            cursorclass=pymysql.cursors.DictCursor
-        )
-    except Exception as e:
-        print(f"DB Connection Error: {e}")
-        return None
-
-def report_telemetry():
-    conn = get_db_connection()
-    if not conn:
-        print("Failed to connect to database for telemetry.")
-        return
-    
-    try:
-        with conn.cursor() as cursor:
-            # Get products count
-            cursor.execute("SELECT COUNT(*) as cnt FROM food_db.products_core")
-            products_count = cursor.fetchone()['cnt']
-            
-            # Get users count
-            cursor.execute("SELECT COUNT(*) as cnt FROM food_db.users")
-            users_count = cursor.fetchone()['cnt']
-            
-            msg = f"TELEMETRY: products_core_count={products_count}, users_count={users_count}"
-            print(f"Sending to Zabbix: {msg}")
-            
-            # Push via SNMP Trap (which is hooked into Zabbix server on 192.168.130.170)
-            notifier.send_alert(msg)
-            
-    except Exception as e:
-        print(f"Telemetry error: {e}")
-    finally:
-        conn.close()
-
-if __name__ == "__main__":
-    report_telemetry()