299 lines
9.1 KiB
Python
299 lines
9.1 KiB
Python
import os
|
|
import sys
|
|
import sqlite3
|
|
import hashlib
|
|
import pandas as pd
|
|
|
|
def get_db_path():
|
|
"""
|
|
Returns the absolute path to the SQLite database.
|
|
If the application is running inside a Docker container, it uses /app/data/nbu_insights.db.
|
|
If running locally, it defaults to a local './data' directory.
|
|
"""
|
|
container_data_dir = "/app/data"
|
|
if os.path.exists(container_data_dir) or os.environ.get("NBU_INSIGHTS_RUNNING") == "1":
|
|
db_dir = container_data_dir
|
|
else:
|
|
base_dir = os.path.dirname(os.path.abspath(__file__))
|
|
db_dir = os.path.join(base_dir, "data")
|
|
|
|
os.makedirs(db_dir, exist_ok=True)
|
|
return os.path.join(db_dir, "nbu_insights.db")
|
|
|
|
def get_connection():
|
|
"""
|
|
Establishes and returns a connection to the SQLite database.
|
|
Rows are configured to be accessible by column names like a dictionary.
|
|
"""
|
|
db_path = get_db_path()
|
|
conn = sqlite3.connect(db_path)
|
|
conn.row_factory = sqlite3.Row
|
|
return conn
|
|
|
|
def hash_password(password, salt=None):
|
|
"""
|
|
Hashes a password using PBKDF2-SHA256 with 100,000 iterations and a unique salt.
|
|
Returns string 'salt_hex:hash_hex' which is safe to store in the DB.
|
|
"""
|
|
if salt is None:
|
|
salt = os.urandom(16)
|
|
elif isinstance(salt, str):
|
|
salt = bytes.fromhex(salt)
|
|
|
|
key = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 100000)
|
|
return salt.hex() + ":" + key.hex()
|
|
|
|
def verify_password(stored_password_hash, provided_password):
|
|
"""
|
|
Verifies a password against its stored PBKDF2-SHA256 hash.
|
|
"""
|
|
try:
|
|
salt_hex, key_hex = stored_password_hash.split(":")
|
|
salt = bytes.fromhex(salt_hex)
|
|
expected_key_hex = hashlib.pbkdf2_hmac('sha256', provided_password.encode('utf-8'), salt, 100000).hex()
|
|
return key_hex == expected_key_hex
|
|
except Exception:
|
|
return False
|
|
|
|
def init_db():
|
|
"""
|
|
Initializes the SQLite database tables according to v2.0/v3.0 specs.
|
|
"""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
# Processed files table
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS processed_files (
|
|
file_hash TEXT PRIMARY KEY,
|
|
file_name TEXT,
|
|
upload_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
""")
|
|
|
|
# Backup jobs table
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS backup_jobs (
|
|
job_id INTEGER PRIMARY KEY,
|
|
client TEXT,
|
|
policy TEXT,
|
|
type TEXT,
|
|
exit_code INTEGER,
|
|
start_time TIMESTAMP,
|
|
finish_time TIMESTAMP,
|
|
duration_secs INTEGER,
|
|
mbytes REAL,
|
|
files_count INTEGER,
|
|
primary_server TEXT,
|
|
media_server TEXT,
|
|
is_rerun_success INTEGER DEFAULT 0
|
|
);
|
|
""")
|
|
|
|
# Technician actions table
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS job_actions (
|
|
job_id INTEGER PRIMARY KEY,
|
|
action_taken TEXT,
|
|
status TEXT DEFAULT 'Pendente',
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY(job_id) REFERENCES backup_jobs(job_id)
|
|
);
|
|
""")
|
|
|
|
# Users table
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
username TEXT PRIMARY KEY,
|
|
password_hash TEXT,
|
|
role TEXT DEFAULT 'user' -- 'admin' or 'user'
|
|
);
|
|
""")
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
def is_file_processed(file_hash):
|
|
"""
|
|
Checks if a CSV file (based on its unique MD5 hash) has already been processed.
|
|
"""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
cursor.execute("SELECT 1 FROM processed_files WHERE file_hash = ?", (file_hash,))
|
|
row = cursor.fetchone()
|
|
conn.close()
|
|
return row is not None
|
|
|
|
def mark_file_processed(file_hash, file_name):
|
|
"""
|
|
Logs a file as processed.
|
|
"""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
cursor.execute(
|
|
"INSERT OR IGNORE INTO processed_files (file_hash, file_name) VALUES (?, ?)",
|
|
(file_hash, file_name)
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
def save_jobs(jobs_df):
|
|
"""
|
|
Saves or updates jobs into the backup_jobs table using SQL UPSERT.
|
|
"""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
for _, row in jobs_df.iterrows():
|
|
start_time = str(row['Start Time']) if not pd.isna(row['Start Time']) else None
|
|
finish_time = str(row['Finish Time']) if not pd.isna(row['Finish Time']) else None
|
|
|
|
cursor.execute("""
|
|
INSERT INTO backup_jobs (
|
|
job_id, client, policy, type, exit_code, start_time, finish_time,
|
|
duration_secs, mbytes, files_count, primary_server, media_server, is_rerun_success
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(job_id) DO UPDATE SET
|
|
client = excluded.client,
|
|
policy = excluded.policy,
|
|
type = excluded.type,
|
|
exit_code = excluded.exit_code,
|
|
start_time = excluded.start_time,
|
|
finish_time = excluded.finish_time,
|
|
duration_secs = excluded.duration_secs,
|
|
mbytes = excluded.mbytes,
|
|
files_count = excluded.files_count,
|
|
primary_server = excluded.primary_server,
|
|
media_server = excluded.media_server,
|
|
is_rerun_success = excluded.is_rerun_success;
|
|
""", (
|
|
int(row['Job ID']),
|
|
row['Client'],
|
|
row['Policy'],
|
|
row['Type'],
|
|
int(row['Exit Code']),
|
|
start_time,
|
|
finish_time,
|
|
int(row['Duration_Sec']),
|
|
float(row['MBytes']),
|
|
int(row['# of Files']),
|
|
row['Primary Server'],
|
|
row['Media Server'],
|
|
int(row['is_rerun_success'])
|
|
))
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
def save_action(job_id, action_taken, status):
|
|
"""
|
|
Saves or updates a mitigation action for a failed backup job.
|
|
"""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
cursor.execute("""
|
|
INSERT INTO job_actions (job_id, action_taken, status, updated_at)
|
|
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
|
|
ON CONFLICT(job_id) DO UPDATE SET
|
|
action_taken = excluded.action_taken,
|
|
status = excluded.status,
|
|
updated_at = CURRENT_TIMESTAMP;
|
|
""", (job_id, action_taken, status))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
def get_historical_jobs():
|
|
"""
|
|
Retrieves all records from the backup_jobs table joined with job_actions.
|
|
"""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
cursor.execute("""
|
|
SELECT j.*, a.action_taken, COALESCE(a.status, 'Pendente') as status, a.updated_at
|
|
FROM backup_jobs j
|
|
LEFT JOIN job_actions a ON j.job_id = a.job_id
|
|
""")
|
|
rows = cursor.fetchall()
|
|
conn.close()
|
|
return [dict(row) for row in rows]
|
|
|
|
# --- User Management CRUD Methods ---
|
|
|
|
def create_user(username, password, role='user'):
|
|
"""
|
|
Hashes the password and creates a new user inside the SQLite store.
|
|
Username is stored in lowercase to ensure uniqueness.
|
|
"""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
password_hash = hash_password(password)
|
|
try:
|
|
cursor.execute(
|
|
"INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)",
|
|
(username.lower().strip(), password_hash, role)
|
|
)
|
|
conn.commit()
|
|
success = True
|
|
except sqlite3.IntegrityError:
|
|
# Username collision
|
|
success = False
|
|
conn.close()
|
|
return success
|
|
|
|
def get_user(username):
|
|
"""
|
|
Retrieves user profile dictionary by username.
|
|
"""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
cursor.execute("SELECT * FROM users WHERE username = ?", (username.lower().strip(),))
|
|
row = cursor.fetchone()
|
|
conn.close()
|
|
return dict(row) if row else None
|
|
|
|
def has_admin_user():
|
|
"""
|
|
Returns True if there is at least one administrator user registered.
|
|
Used for routing to the initial bootstrap setup screen.
|
|
"""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
cursor.execute("SELECT 1 FROM users WHERE role = 'admin' LIMIT 1")
|
|
row = cursor.fetchone()
|
|
conn.close()
|
|
return row is not None
|
|
|
|
def get_all_users():
|
|
"""
|
|
Returns lists of all user details (excluding their hashed passwords).
|
|
"""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
cursor.execute("SELECT username, role FROM users ORDER BY username")
|
|
rows = cursor.fetchall()
|
|
conn.close()
|
|
return [dict(row) for row in rows]
|
|
|
|
def delete_user(username):
|
|
"""
|
|
Removes a user by their username.
|
|
"""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
cursor.execute("DELETE FROM users WHERE username = ?", (username.lower().strip(),))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
def reset_all_data():
|
|
"""
|
|
Destructive helper: Truncates all backup jobs, processed file hashes,
|
|
and technician actions. User accounts are NOT deleted.
|
|
"""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
cursor.execute("DELETE FROM backup_jobs")
|
|
cursor.execute("DELETE FROM job_actions")
|
|
cursor.execute("DELETE FROM processed_files")
|
|
conn.commit()
|
|
conn.close()
|