Files
SnarfCode/scripts/remediate_heimdall.py

139 lines
4.4 KiB
Python

import paramiko
import sys
import time
sys.stdout.reconfigure(encoding='utf-8')
key_path = r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet'
def main():
print("=== Connecting to Shipyard Swarm Manager (140.44.4.71) for Heimdall Remediation ===")
c = paramiko.SSHClient()
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
c.connect("140.44.4.71", username="root", key_filename=key_path, timeout=10)
def run(cmd):
print(f"\n>>> {cmd}")
_, o, e = c.exec_command(cmd)
res = o.read().decode().strip()
err = e.read().decode().strip()
if res:
print(res)
if err:
print("[stderr]", err)
return res
# 1. Ensure backup directory exists & take safety backup
run("mkdir -p /mnt/nfs_shares/backups")
run("cp -v /mnt/nfs_shares/heimdall/www/app.sqlite /mnt/nfs_shares/backups/heimdall_pre_remediation.sqlite")
# 2. Database repair script executed locally on node
db_clean_script = '''
import sqlite3
import shutil
db_path = "/mnt/nfs_shares/heimdall/www/app.sqlite"
con = sqlite3.connect(db_path)
cur = con.cursor()
print("Initial total items:", cur.execute("SELECT count(*) FROM items").fetchone()[0])
# Delete all soft-deleted items (deleted_at IS NOT NULL)
cur.execute("DELETE FROM items WHERE deleted_at IS NOT NULL")
print("Deleted soft-deleted items. Remaining:", cur.execute("SELECT count(*) FROM items").fetchone()[0])
# Remove legacy Pi-hole items (Pi-hole 1 and Pi-hole 2) since Pi-hole stack is decommissioned
cur.execute("DELETE FROM items WHERE title LIKE 'Pi-hole%'")
print("Purged legacy Pi-hole items. Remaining:", cur.execute("SELECT count(*) FROM items").fetchone()[0])
# Deduplicate items by title just in case, keeping the highest ID
cur.execute("""
DELETE FROM items
WHERE id NOT IN (
SELECT MAX(id)
FROM items
GROUP BY title
) AND id != 0
""")
# Re-sequence the 'order' field sequentially
rows = cur.execute("SELECT id FROM items WHERE id != 0 ORDER BY [order] ASC, title ASC").fetchall()
for idx, (item_id,) in enumerate(rows):
cur.execute("UPDATE items SET [order] = ? WHERE id = ?", (idx, item_id))
con.commit()
cur.execute("REINDEX")
cur.execute("VACUUM")
con.close()
print("Database sanitized, re-indexed, and vacuumed successfully!")
'''
sftp = c.open_sftp()
with sftp.file('/tmp/clean_heimdall_db.py', 'w') as f:
f.write(db_clean_script)
sftp.close()
run("python3 /tmp/clean_heimdall_db.py")
# 3. Create local high-performance storage directory on snshpd4lnxswm01
run("mkdir -p /var/lib/docker-data/heimdall/config")
run("rsync -av --progress /mnt/nfs_shares/heimdall/ /var/lib/docker-data/heimdall/config/")
run("chown -R 1000:1000 /var/lib/docker-data/heimdall/config")
run("ls -la /var/lib/docker-data/heimdall/config/www/")
# 4. Generate updated Docker Compose file with node pinning & local volume
new_compose = '''version: "3.8"
services:
heimdall:
image: lscr.io/linuxserver/heimdall:latest
ports:
- target: 80
published: 85
mode: ingress
- target: 443
published: 403
mode: ingress
environment:
- PUID=1000
- PGID=1000
- TZ=America/New_York
volumes:
- type: bind
source: /var/lib/docker-data/heimdall/config
target: /config
deploy:
mode: replicated
replicas: 1
placement:
constraints:
- node.hostname == snshpd4lnxswm01
restart_policy:
condition: on-failure
networks:
default:
name: homelab_public
external: true
'''
sftp = c.open_sftp()
with sftp.file('/tmp/heimdall_compose.yml', 'w') as f:
f.write(new_compose)
sftp.close()
# 5. Redeploy Heimdall stack
run("docker stack deploy -c /tmp/heimdall_compose.yml heimdall")
time.sleep(5)
run("docker stack ps heimdall")
# 6. Setup daily atomic SQLite backup cron job
cron_cmd = '(crontab -l 2>/dev/null | grep -v "heimdall_backup"; echo "30 3 * * * python3 -c \\"import sqlite3; con=sqlite3.connect(\'/var/lib/docker-data/heimdall/config/www/app.sqlite\'); b=sqlite3.connect(\'/mnt/nfs_shares/backups/heimdall_daily.sqlite\'); con.backup(b); b.close(); con.close()\\" >/dev/null 2>&1") | crontab -'
run(cron_cmd)
run("crontab -l | grep heimdall")
c.close()
print("\n=== Heimdall Storage & DB Remediation Complete ===")
if __name__ == '__main__':
main()