Compare commits

...
5 Commits
Author SHA1 Message Date
bart 2fca819dff chore: add generic sqlite to postgres migration script 2026-10-18 16:43:20 +02:00
bart ff83a88c6d feat(lidarr): add data migration and host cleanup scripts 2026-10-18 16:43:01 +02:00
bart 1115dc8542 chore(lidarr): scale to 1 after database migration
Sync HTPC Manifests to GitOps / Sync Manifests to GitOps Repo (push) Successful in 9s
2026-10-18 16:33:01 +02:00
bart 4d91c34bf7 chore(lidarr): scale to 0 for database migration
Sync HTPC Manifests to GitOps / Sync Manifests to GitOps Repo (push) Successful in 4s
2026-10-18 16:14:45 +02:00
bart 2ef3da29f2 feat(lidarr): add Kubernetes manifests for Lidarr with PostgreSQL backend
Sync HTPC Manifests to GitOps / Sync Manifests to GitOps Repo (push) Successful in 4s
2026-10-18 16:09:50 +02:00
9 changed files with 475 additions and 0 deletions
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
# ==============================================================================
# Opruimen van oude standalone Lidarr installatie op vmserver (10.2.0.4)
# ==============================================================================
set -euo pipefail
CLUSTER_HOST="10.2.0.4"
SSH_USER="bart"
COLOR_GREEN="\033[0;32m"
COLOR_YELLOW="\033[1;33m"
COLOR_RED="\033[0;31m"
COLOR_BLUE="\033[0;34m"
COLOR_RESET="\033[0m"
log_info() { echo -e "${COLOR_GREEN}[INFO]${COLOR_RESET} $1"; }
log_step() { echo -e "\n${COLOR_BLUE}==>${COLOR_RESET} ${COLOR_GREEN}$1${COLOR_RESET}"; }
log_warn() { echo -e "${COLOR_YELLOW}[WARN]${COLOR_RESET} $1"; }
echo "========================================================================"
echo " OPRUIMEN OUDE STANDALONE LIDARR OP $CLUSTER_HOST"
echo "========================================================================"
echo "Dit stopt en verwijdert de oude standalone bestanden op $CLUSTER_HOST:"
echo " - /etc/systemd/system/lidarr.service"
echo " - /opt/Lidarr"
echo " - /storage/var/lib/lidarr"
echo "Er is al een reservekopie gemaakt in /storage/backups/lidarr_complete_backup_2026-10-18.tar.gz"
echo "========================================================================"
echo ""
read -p "Weet je zeker dat je de oude Lidarr bestanden wilt verwijderen? (j/N): " -r CONFIRM
if [[ ! "$CONFIRM" =~ ^[jJyY]$ ]]; then
log_info "Opruimen geannuleerd."
exit 0
fi
log_step "Verwijderen van systemd service en programmabestanden op ${CLUSTER_HOST}..."
ssh -t "${SSH_USER}@${CLUSTER_HOST}" "
echo '--> Stoppen en uitschakelen van lidarr.service...'
sudo systemctl stop lidarr 2>/dev/null || true
sudo systemctl disable lidarr 2>/dev/null || true
sudo rm -f /etc/systemd/system/lidarr.service
sudo systemctl daemon-reload
sudo systemctl reset-failed 2>/dev/null || true
echo '--> Verwijderen van /opt/Lidarr...'
sudo rm -rf /opt/Lidarr
echo '--> Verwijderen van /storage/var/lib/lidarr...'
sudo rm -rf /storage/var/lib/lidarr
echo '--> Opslagruimte controleren...'
df -h / /storage
"
echo ""
log_info "De oude standalone Lidarr is succesvol en volledig opgeruimd!"
echo "Alle verkeer naar https://thuus.bnhmeijer.nl/lidarr/ loopt nu direct via de Kubernetes pod."
+1
View File
@@ -7,5 +7,6 @@ resources:
- namespace.yaml - namespace.yaml
- radarr - radarr
- sonarr - sonarr
- lidarr
+84
View File
@@ -0,0 +1,84 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: lidarr
namespace: htpc
labels:
app.kubernetes.io/name: lidarr
app.kubernetes.io/part-of: htpc
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: lidarr
template:
metadata:
labels:
app: lidarr
app.kubernetes.io/name: lidarr
app.kubernetes.io/part-of: htpc
spec:
nodeSelector:
kubernetes.io/hostname: vmserver
containers:
- name: lidarr
image: lscr.io/linuxserver/lidarr:latest
imagePullPolicy: IfNotPresent
env:
- name: PUID
value: "33"
- name: PGID
value: "33"
- name: TZ
value: "Europe/Amsterdam"
- name: UMASK
value: "0002"
envFrom:
- secretRef:
name: lidarr-secret
ports:
- name: http
containerPort: 8686
hostPort: 8686
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 2000m
memory: 2Gi
volumeMounts:
- name: config
mountPath: /config
- name: datadisk
mountPath: /datadisk
- name: mnt-mp3
mountPath: /mnt/mp3
livenessProbe:
httpGet:
path: /lidarr/ping
port: 8686
initialDelaySeconds: 60
periodSeconds: 20
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /lidarr/ping
port: 8686
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
volumes:
- name: config
persistentVolumeClaim:
claimName: lidarr-config
- name: datadisk
hostPath:
path: /datadisk
type: Directory
- name: mnt-mp3
hostPath:
path: /mnt/mp3
type: Directory
+8
View File
@@ -0,0 +1,8 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- secret.yaml
- pvc.yaml
- service.yaml
- deployment.yaml
+15
View File
@@ -0,0 +1,15 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: lidarr-config
namespace: htpc
labels:
app.kubernetes.io/name: lidarr
app.kubernetes.io/part-of: htpc
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 10Gi
+20
View File
@@ -0,0 +1,20 @@
apiVersion: v1
kind: Secret
metadata:
name: lidarr-secret
namespace: htpc
labels:
app.kubernetes.io/name: lidarr
app.kubernetes.io/part-of: htpc
type: Opaque
stringData:
LIDARR__POSTGRES__HOST: "postgresql.cluster-infra.svc.cluster.local"
LIDARR__POSTGRES__PORT: "5432"
LIDARR__POSTGRES__USER: "lidarr"
LIDARR__POSTGRES__PASSWORD: "lidarr_pg_pass_2026"
LIDARR__POSTGRES__MAINDB: "lidarr-main"
LIDARR__POSTGRES__LOGDB: "lidarr-log"
LIDARR__SERVER__URLBASE: "lidarr"
LIDARR__SERVER__PORT: "8686"
LIDARR__SERVER__ALLOWEDHOSTS: "*"
LIDARR__AUTH__APIKEY: "463c1d257f6a473fb41a23fa34ca762b"
+17
View File
@@ -0,0 +1,17 @@
apiVersion: v1
kind: Service
metadata:
name: lidarr
namespace: htpc
labels:
app.kubernetes.io/name: lidarr
app.kubernetes.io/part-of: htpc
spec:
type: ClusterIP
ports:
- port: 8686
targetPort: 8686
protocol: TCP
name: http
selector:
app: lidarr
+146
View File
@@ -0,0 +1,146 @@
#!/usr/bin/env python3
import sqlite3
import csv
import sys
from collections import defaultdict
def escape_pg_string(s):
if s is None:
return "NULL"
if isinstance(s, bytes):
s = s.decode('utf-8', errors='replace')
s = str(s).replace("'", "''")
return f"'{s}'"
def main():
sqlite_db_path = "/tmp/lidarr.db"
pg_columns_path = "/tmp/pg_lidarr_columns.csv"
output_sql_path = "/tmp/migrate_lidarr_data.sql"
# 1. Lees PG tabellen en kolommen
pg_schema = defaultdict(dict)
with open(pg_columns_path, mode='r', encoding='utf-8') as f:
reader = csv.reader(f)
for row in reader:
if not row or len(row) < 3:
continue
table, col, dtype = row[0].strip(), row[1].strip(), row[2].strip()
pg_schema[table][col] = dtype
# 2. Open SQLite
conn = sqlite3.connect(f"file:{sqlite_db_path}?mode=ro&immutable=1", uri=True)
cur = conn.cursor()
cur.execute("SELECT name FROM sqlite_master WHERE type='table';")
sqlite_tables = set(r[0] for r in cur.fetchall())
with open(output_sql_path, mode='w', encoding='utf-8') as out:
out.write("-- Lidarr SQLite naar PostgreSQL Data Migratie\n")
out.write("BEGIN;\n")
out.write("SET session_replication_role = 'replica';\n\n")
# Tabellen die we NIET moeten overschrijven
skip_tables = {"VersionInfo"}
total_rows_imported = 0
for table, pg_cols in pg_schema.items():
if table in skip_tables:
continue
out.write(f"-- Tabel: {table}\n")
out.write(f'TRUNCATE TABLE "{table}" CASCADE;\n')
if table == "DelayProfiles":
out.write('INSERT INTO "DelayProfiles" ("Id", "EnableUsenet", "EnableTorrent", "PreferredProtocol", "UsenetDelay", "TorrentDelay", "Order", "Tags", "BypassIfHighestQuality", "BypassIfAboveCustomFormatScore", "MinimumCustomFormatScore") VALUES (1, true, true, 1, 0, 0, 2147483647, \'[]\', true, false, 0);\n\n')
total_rows_imported += 1
continue
if table not in sqlite_tables:
out.write(f"-- Tabel {table} bestaat niet in SQLite, leeggemaakt en overgeslagen.\n\n")
continue
# Haal SQLite kolommen op
cur.execute(f'PRAGMA table_info("{table}");')
sqlite_col_names = set(r[1] for r in cur.fetchall())
# Gemeenschappelijke kolommen
common_cols = [c for c in pg_cols.keys() if c in sqlite_col_names]
if not common_cols:
out.write(f"-- Geen gemeenschappelijke kolommen voor {table}\n\n")
continue
# Query data uit SQLite
cols_quoted = [f'"{c}"' for c in common_cols]
cur.execute(f'SELECT {", ".join(cols_quoted)} FROM "{table}";')
rows = cur.fetchall()
if not rows:
out.write(f"-- Geen rijen in SQLite voor {table}\n\n")
continue
out.write(f'-- {len(rows)} rijen migreren voor {table}\n')
total_rows_imported += len(rows)
chunk_size = 500
for i in range(0, len(rows), chunk_size):
chunk = rows[i:i + chunk_size]
out.write(f'INSERT INTO "{table}" ({", ".join([f"""\"{c}\"""" for c in common_cols])}) VALUES\n')
val_lines = []
for row_data in chunk:
val_parts = []
for val, col_name in zip(row_data, common_cols):
dtype = pg_cols[col_name].lower()
if val is None:
val_parts.append("NULL")
elif "bool" in dtype:
val_parts.append("TRUE" if str(val) in ("1", "True", "true") else "FALSE")
elif dtype in ("integer", "bigint", "smallint", "numeric", "double precision", "real"):
s = str(val).strip()
if s == "TorrentDownloadProtocol":
val_parts.append("2")
elif s == "SoulseekDownloadProtocol":
val_parts.append("3")
elif s == "UsenetDownloadProtocol":
val_parts.append("1")
else:
try:
val_parts.append(str(int(s)))
except ValueError:
try:
float(s)
val_parts.append(s)
except ValueError:
val_parts.append("0")
else:
val_parts.append(escape_pg_string(val))
val_lines.append(f' ({", ".join(val_parts)})')
out.write(",\n".join(val_lines) + ";\n")
out.write("\n")
# Sequences resetten
out.write("-- Sequences bijwerken naar MAX(Id)\n")
out.write("""DO $$
DECLARE
r RECORD;
has_seq text;
BEGIN
FOR r IN SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name != 'VersionInfo' LOOP
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = r.table_name AND column_name = 'Id') THEN
has_seq := pg_get_serial_sequence('"' || r.table_name || '"', 'Id');
IF has_seq IS NOT NULL THEN
EXECUTE format('SELECT setval(%L, coalesce(max("Id"), 1)) FROM %I', has_seq, r.table_name);
END IF;
END IF;
END LOOP;
END $$;\n\n""")
out.write("SET session_replication_role = 'origin';\n")
out.write("COMMIT;\n")
print(f"SQL dump succesvol gegenereerd: {output_sql_path}")
print(f"Totaal aantal rijen klaargezet voor import: {total_rows_imported}")
if __name__ == "__main__":
main()
+126
View File
@@ -0,0 +1,126 @@
#!/usr/bin/env python3
import sqlite3
import csv
import sys
from collections import defaultdict
def escape_pg_string(s):
if s is None:
return "NULL"
if isinstance(s, bytes):
s = s.decode('utf-8', errors='replace')
s = str(s).replace("'", "''")
return f"'{s}'"
def main():
sqlite_db_path = "/tmp/radarr.db"
pg_columns_path = "/tmp/pg_columns.csv"
output_sql_path = "/tmp/migrate_radarr_data.sql"
# 1. Lees PG tabellen en kolommen
pg_schema = defaultdict(dict)
with open(pg_columns_path, mode='r', encoding='utf-8') as f:
reader = csv.reader(f)
for row in reader:
if not row or len(row) < 3:
continue
table, col, dtype = row[0].strip(), row[1].strip(), row[2].strip()
pg_schema[table][col] = dtype
# 2. Open SQLite
conn = sqlite3.connect(f"file:{sqlite_db_path}?mode=ro&immutable=1", uri=True)
cur = conn.cursor()
cur.execute("SELECT name FROM sqlite_master WHERE type='table';")
sqlite_tables = set(r[0] for r in cur.fetchall())
with open(output_sql_path, mode='w', encoding='utf-8') as out:
out.write("-- Radarr SQLite naar PostgreSQL Data Migratie\n")
out.write("BEGIN;\n")
out.write("SET session_replication_role = 'replica';\n\n")
# Tabellen die we NIET moeten overschrijven
skip_tables = {"VersionInfo"}
total_rows_imported = 0
for table, pg_cols in pg_schema.items():
if table in skip_tables:
continue
out.write(f"-- Tabel: {table}\n")
out.write(f'TRUNCATE TABLE "{table}" CASCADE;\n')
if table not in sqlite_tables:
out.write(f"-- Tabel {table} bestaat niet in SQLite, leeggemaakt en overgeslagen.\n\n")
continue
# Haal SQLite kolommen op
cur.execute(f'PRAGMA table_info("{table}");')
sqlite_col_names = set(r[1] for r in cur.fetchall())
# Gemeenschappelijke kolommen
common_cols = [c for c in pg_cols.keys() if c in sqlite_col_names]
if not common_cols:
out.write(f"-- Geen gemeenschappelijke kolommen voor {table}\n\n")
continue
# Query data uit SQLite
cols_quoted = [f'"{c}"' for c in common_cols]
cur.execute(f'SELECT {", ".join(cols_quoted)} FROM "{table}";')
rows = cur.fetchall()
if not rows:
out.write(f"-- Geen rijen in SQLite voor {table}\n\n")
continue
out.write(f'-- {len(rows)} rijen migreren voor {table}\n')
total_rows_imported += len(rows)
chunk_size = 200
for i in range(0, len(rows), chunk_size):
chunk = rows[i:i + chunk_size]
out.write(f'INSERT INTO "{table}" ({", ".join([f"""\"{c}\"""" for c in common_cols])}) VALUES\n')
val_lines = []
for row_data in chunk:
val_parts = []
for val, col_name in zip(row_data, common_cols):
dtype = pg_cols[col_name].lower()
if val is None:
val_parts.append("NULL")
elif "bool" in dtype:
val_parts.append("TRUE" if str(val) in ("1", "True", "true") else "FALSE")
elif dtype in ("integer", "bigint", "smallint", "numeric", "double precision", "real"):
val_parts.append(str(val))
else:
val_parts.append(escape_pg_string(val))
val_lines.append(f' ({", ".join(val_parts)})')
out.write(",\n".join(val_lines) + ";\n")
out.write("\n")
# Sequences resetten
out.write("-- Sequences bijwerken naar MAX(Id)\n")
out.write("""DO $$
DECLARE
r RECORD;
has_seq text;
BEGIN
FOR r IN SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name != 'VersionInfo' LOOP
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = r.table_name AND column_name = 'Id') THEN
has_seq := pg_get_serial_sequence('"' || r.table_name || '"', 'Id');
IF has_seq IS NOT NULL THEN
EXECUTE format('SELECT setval(%L, coalesce(max("Id"), 1)) FROM %I', has_seq, r.table_name);
END IF;
END IF;
END LOOP;
END $$;\n\n""")
out.write("SET session_replication_role = 'origin';\n")
out.write("COMMIT;\n")
print(f"SQL dump succesvol gegenereerd: {output_sql_path}")
print(f"Totaal aantal rijen klaargezet voor import: {total_rows_imported}")
if __name__ == "__main__":
main()