Преглед изворни кода

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

lanfr144 пре 1 месец
родитељ
комит
ade82aff87
11 измењених фајлова са 68 додато и 502 уклоњено
  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

+ 3 - 4
app.py

@@ -143,7 +143,7 @@ def verify_login(username, password):
     conn = get_db_connection('app_auth')
     conn = get_db_connection('app_auth')
     if not conn: return False
     if not conn: return False
     with conn.cursor() as cursor:
     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()
         result = cursor.fetchone()
         conn.close()
         conn.close()
         if result: return bcrypt.checkpw(password.encode('utf-8'), result['password_hash'].encode('utf-8'))
         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')
     conn = get_db_connection('app_auth')
     if not conn: return None
     if not conn: return None
     with conn.cursor() as cursor:
     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()
         result = cursor.fetchone()
         conn.close()
         conn.close()
         return result['id'] if result else None
         return result['id'] if result else None
@@ -172,7 +172,7 @@ def get_user_limit(username):
     conn = get_db_connection('app_auth')
     conn = get_db_connection('app_auth')
     if not conn: return "50"
     if not conn: return "50"
     with conn.cursor() as cursor:
     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()
         result = cursor.fetchone()
         conn.close()
         conn.close()
         return result['search_limit'] if (result and result['search_limit']) else "50"
         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
                             WHERE m.proteins_100g IS NOT NULL AND m.fat_100g IS NOT NULL AND m.carbohydrates_100g IS NOT NULL
                             {wh_comp}
                             {wh_comp}
                             {order_by}
                             {order_by}
-                            LIMIT 15
                         """
                         """
                         cursor.execute(sql, (bool_search,))
                         cursor.execute(sql, (bool_search,))
                         return cursor.fetchall()
                         return cursor.fetchall()

+ 7 - 2
backup_db.sh

@@ -17,8 +17,13 @@ echo "Starting Database Backup for $DB_NAME..."
 # Ensure backup directory exists
 # Ensure backup directory exists
 mkdir -p "$BACKUP_DIR"
 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
 if [ $? -eq 0 ]; then
     echo "Backup successfully saved to $BACKUP_FILE"
     echo "Backup successfully saved to $BACKUP_FILE"

+ 0 - 0
configure_zabbix_email.py


+ 56 - 25
data_sync.sh

@@ -1,29 +1,25 @@
 #!/bin/bash
 #!/bin/bash
-# $Id$
-# $Author$
-# $log$
-#ident "@(#)LocalFoodAI:data_sync.sh:$Format:%D:%ci:%cN:%h$"
 # data_sync.sh - Automated Data Freshness Pipeline
 # 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 ---
 # --- Auto-Detach & Sudo Auth Block ---
 if [ "$1" = "--detached" ]; then
 if [ "$1" = "--detached" ]; then
     shift # Remove --detached from arguments for normal parsing
     shift # Remove --detached from arguments for normal parsing
 else
 else
     echo "Preparing to run ingestion in the background to survive SSH disconnections."
     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 "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
     exit 0
 fi
 fi
 # -------------------------------------
 # -------------------------------------
 
 
+if [ -f "./.env" ]; then
+    source ./.env
+fi
 
 
 ONLINE_MODE=0
 ONLINE_MODE=0
 DATA_DIR="./data"
 DATA_DIR="./data"
@@ -39,17 +35,17 @@ while [[ "$#" -gt 0 ]]; do
     shift
     shift
 done
 done
 
 
-echo "Starting Data Freshness Sync..."
+echo "Starting Data Freshness Sync at $(date)..."
 mkdir -p "$DATA_DIR"
 mkdir -p "$DATA_DIR"
 
 
 if [ "$ONLINE_MODE" -eq 1 ]; then
 if [ "$ONLINE_MODE" -eq 1 ]; then
     echo "Online mode enabled. Checking for latest OpenFoodFacts database..."
     echo "Online mode enabled. Checking for latest OpenFoodFacts database..."
     if ping -c 1 google.com &> /dev/null; then
     if ping -c 1 google.com &> /dev/null; then
         echo "Internet connection verified. Downloading latest dataset..."
         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
         if [ $? -eq 0 ]; then
-            echo "Download complete."
+            echo "Download check complete."
         else
         else
             echo "Failed to download dataset."
             echo "Failed to download dataset."
         fi
         fi
@@ -62,16 +58,51 @@ fi
 
 
 # Check if file exists to trigger ingestion
 # Check if file exists to trigger ingestion
 if [ -f "$DATA_DIR/$INGEST_FILE" ]; then
 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 "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
 else
     echo "No dataset found in $DATA_DIR. Nothing to ingest."
     echo "No dataset found in $DATA_DIR. Nothing to ingest."
 fi
 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_USER: "your_email@example.com"
 
 
   EMAIL_PASS: "placeholder_email_pass"
   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()