From 03011670a3d0c8491fe46014b5946716991b49a97996c1e16d5f9e4347bf26fb Mon Sep 17 00:00:00 2001 From: mkt Date: Thu, 4 Jun 2026 09:28:26 +0200 Subject: [PATCH] Removed EspoCRM GUI elements risky for tags; new restore script --- create_pod_traefik.sh | 85 +++++++++++++++++++++ restore_espocrm.sh | 170 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 255 insertions(+) create mode 100755 create_pod_traefik.sh create mode 100755 restore_espocrm.sh diff --git a/create_pod_traefik.sh b/create_pod_traefik.sh new file mode 100755 index 0000000..9709708 --- /dev/null +++ b/create_pod_traefik.sh @@ -0,0 +1,85 @@ +#!/bin/bash + +# To be run by user trf to create the pod and container with +# Traefik and to create the systemd service + +# Environment variables +POD_NAME='traefik_pod' +CTR_NAME='traefik_ctr' +IMAGE='docker.io/library/traefik:v3.5.4' +TRAEFIK_HOST_PORT='8080' +DASH_HOST_PORT='8085' +HOST_INGRESS_IP='10.8.0.6' +HOST_LOCAL_IP='127.0.0.1' +NET_OPTS='slirp4netns:allow_host_loopback=true,port_handler=slirp4netns' +BIND_DIR="$HOME/.local/share/$POD_NAME" +DYNAMIC_DIR="$BIND_DIR/dynamic" +STATIC_CFG="$BIND_DIR/traefik.yml" +USER_SYSTEMD_DIR="$HOME/.config/systemd/user" +POD_UNIT_FILE="$USER_SYSTEMD_DIR/pod-${POD_NAME}.service" + +# Prepare directories +mkdir -p "$BIND_DIR" "$DYNAMIC_DIR" "$USER_SYSTEMD_DIR" + +# Create pod if not yet existing +if ! podman pod exists "$POD_NAME"; then + podman pod create \ + -n "$POD_NAME" \ + --network "$NET_OPTS" \ + -p ${HOST_INGRESS_IP}:${TRAEFIK_HOST_PORT}:${TRAEFIK_HOST_PORT} \ + -p ${HOST_LOCAL_IP}:${DASH_HOST_PORT}:${DASH_HOST_PORT} + echo "Pod '$POD_NAME' created (rc=$?)" +else + echo "Pod '$POD_NAME' already exists." +fi + +# Traefik container +# Remove old container +podman rm -f "$CTR_NAME" +# New container +podman run -d --name "$CTR_NAME" --pod "$POD_NAME" \ + -v "$STATIC_CFG":/etc/traefik/traefik.yml:ro \ + -v "$DYNAMIC_DIR":/etc/traefik/dynamic:ro \ + "$IMAGE" +echo "Started $CTR_NAME container (rc=$?)" + +# Generate systemd service files +cd "$USER_SYSTEMD_DIR" +podman generate systemd --files --new --name "$POD_NAME" +echo "Generated systemd service files (rc=$?)" + +# Inject WireGuard IP readiness check into pod-${POD_NAME}.service +awk -v ip="$HOST_INGRESS_IP" ' +BEGIN { inserted=0 } +# Insert the readiness check immediately BEFORE the pod create ExecStartPre +/^ExecStartPre=\/usr\/bin\/podman pod create/ && inserted==0 { + # Wait up to ~60s for ip to show up on any interface (safer than assuming wg0) + # Uses grep -F to avoid regex escaping issues with dots in the IP. + print "ExecStartPre=/bin/sh -ceu \047for i in $(seq 1 30); do ip -4 addr show | grep -Fq \" " ip "/\" && exit 0; sleep 2; done; echo \"IP " ip " not up\" >&2; exit 1\047" + inserted=1 +} +{ print } +' "$POD_UNIT_FILE" > "${POD_UNIT_FILE}.tmp" +mv "${POD_UNIT_FILE}.tmp" "$POD_UNIT_FILE" +echo "Injected WG IP readiness check for ${HOST_INGRESS_IP} into pod unit" + +# Stop & remove live pod and containers +podman pod stop --ignore --time 15 "$POD_NAME" +podman pod rm -f --ignore "$POD_NAME" +if podman pod exists "$POD_NAME"; then + echo "ERROR: Pod $POD_NAME still exists." + exit 1 +else + echo "Stopped & removed live pod $POD_NAME and containers" +fi + +# Enable systemd services +systemctl --user daemon-reload +systemctl --user enable --now "pod-${POD_NAME}.service" +echo "Enabled systemd service pod-${POD_NAME}.service (rc=$?)" +echo "Status: systemctl --user status pod-${POD_NAME}.service" +echo "View logs: journalctl --user -u pod-${POD_NAME}.service -f" +systemctl --user enable --now "container-${CTR_NAME}.service" +echo "Enabled systemd service container-${CTR_NAME}.service (rc=$?)" +echo "Status: systemctl --user status container-${CTR_NAME}.service" +echo "View logs: journalctl --user -u container-${CTR_NAME}.service -f" diff --git a/restore_espocrm.sh b/restore_espocrm.sh new file mode 100755 index 0000000..7bd084f --- /dev/null +++ b/restore_espocrm.sh @@ -0,0 +1,170 @@ +#!/bin/bash + +# To be run by user mkt to restore the EspoCRM pod from a backup made by +# backup_espocrm.sh (a MariaDB dump + a tarball of the EspoCRM data dir). +# Counterpart of backup_espocrm.sh. DANGEROUS: overwrites the current DB and +# the current EspoCRM data dir with the backed-up state. +# +# Usage: +# restore_espocrm.sh [TIMESTAMP] # restore a given set (default: latest) +# restore_espocrm.sh --list # list available backup sets +# restore_espocrm.sh --yes [TS] # skip the confirmation prompt +# +# A backup set is the pair of files +# $BAK_DIR/espocrm-db-.sql.gz and $BAK_DIR/espocrm-files-.tar.gz + +# Environment variables +POD_NAME='espocrm_pod' +DB_CTR='mariadb_ctr' +WEB_CTR='espocrm_ctr' +DAEMON_CTR='espocrm_daemon_ctr' +ESPO_IMAGE='docker.io/espocrm/espocrm:9.3.8' + +HOST_LOCAL_IP='127.0.0.1' +WEB_HOST_PORT='8093' +SITE_URL="http://${HOST_LOCAL_IP}:${WEB_HOST_PORT}" + +BIND_DIR="$HOME/.local/share/$POD_NAME" +ESPO_DATA_DIR="$BIND_DIR/espocrm" # -> /var/www/html +DB_NAME='espocrm' +BAK_DIR="$HOME/bak" + +# systemd --user service names +POD_SVC="pod-${POD_NAME}.service" +DB_SVC="container-${DB_CTR}.service" +WEB_SVC="container-${WEB_CTR}.service" +DAEMON_SVC="container-${DAEMON_CTR}.service" + +# List available backup sets (timestamps that have BOTH a db and a files file) +list_sets() { + local f ts + for f in "$BAK_DIR"/espocrm-db-*.sql.gz; do + [ -e "$f" ] || continue + ts=${f##*/espocrm-db-}; ts=${ts%.sql.gz} + [ -e "$BAK_DIR/espocrm-files-$ts.tar.gz" ] && echo "$ts" + done | sort +} + +# Parse arguments +ASSUME_YES=0 +TS='' +for arg in "$@"; do + case "$arg" in + -l|--list) + echo "Available backup sets in $BAK_DIR:" + list_sets | sed 's/^/ /' + exit 0 + ;; + -y|--yes) ASSUME_YES=1 ;; + -*) echo "Unknown option: $arg" >&2; exit 2 ;; + *) TS="$arg" ;; + esac +done + +# Resolve the timestamp (default: latest set) +if [ -z "$TS" ]; then + TS=$(list_sets | tail -n 1) + if [ -z "$TS" ]; then + echo "ERROR: no backup sets found in $BAK_DIR." >&2 + echo "Run backup_espocrm.sh first." >&2 + exit 1 + fi + echo "No timestamp given; using latest set: $TS" +fi + +DB_BAK="$BAK_DIR/espocrm-db-$TS.sql.gz" +FILES_BAK="$BAK_DIR/espocrm-files-$TS.tar.gz" + +# Verify the chosen set exists and is intact BEFORE touching the live system +for f in "$DB_BAK" "$FILES_BAK"; do + if [ ! -f "$f" ]; then + echo "ERROR: backup file not found: $f" >&2 + echo "Available sets:" >&2; list_sets | sed 's/^/ /' >&2 + exit 1 + fi +done +gzip -t "$DB_BAK" || { echo "ERROR: $DB_BAK is corrupt (gzip -t failed)." >&2; exit 1; } +gzip -t "$FILES_BAK" || { echo "ERROR: $FILES_BAK is corrupt (gzip -t failed)." >&2; exit 1; } +echo "Backup set '$TS' found and verified:" +echo " $DB_BAK" +echo " $FILES_BAK" + +# Confirmation (overwrites current data) +if [ "$ASSUME_YES" -ne 1 ]; then + read -r -p "WARNING: this OVERWRITES the current DB '$DB_NAME' and ALL files under $ESPO_DATA_DIR with backup '$TS'. To confirm, type 'RESTORE espocrm_pod': " C + [ "$C" = "RESTORE espocrm_pod" ] || { echo "Aborted."; exit 1; } +fi + +# Make sure the pod and the DB are up (DB is needed for the import). +systemctl --user start "$POD_SVC" "$DB_SVC" +echo "Ensured pod and DB are running (rc=$?)" +echo "Waiting for MariaDB to be ready..." +for i in $(seq 1 60); do + podman exec "$DB_CTR" mariadb-admin ping --silent >/dev/null 2>&1 && break + sleep 2 +done +echo "MariaDB ping ready (rc=$?)" + +# Stop the web and daemon containers so nothing uses the data dir / DB mid-restore. +systemctl --user stop "$DAEMON_SVC" "$WEB_SVC" +echo "Stopped $WEB_SVC and $DAEMON_SVC (rc=$?)" + +# --- Restore the EspoCRM data dir --------------------------------------------- +# Clear the data dir and extract the tarball INSIDE a container that mounts it, +# so the bind-mount ownership is handled inside the user namespace. Use +# --no-same-owner so files land owned by the container root (-> host user mkt), +# matching the official image's code layout. Then chown the writable resources +# (data, custom, client/custom; config.php lives under data) to www-data, so the +# app can write its cache/logs/config. The entrypoint does NOT re-chown existing +# files, so we do it here explicitly. +echo "Restoring EspoCRM files from $FILES_BAK ..." +podman run --rm -i --entrypoint sh \ + -v "$ESPO_DATA_DIR":/var/www/html \ + "$ESPO_IMAGE" -c ' + set -e + find /var/www/html -mindepth 1 -delete + tar -xzf - --no-same-owner -C /var/www/html + for d in data custom client/custom; do + [ -e "/var/www/html/$d" ] && chown -R www-data:www-data "/var/www/html/$d" + done + ' < "$FILES_BAK" +echo "Restored EspoCRM files (rc=$?)" + +# --- Restore the database ----------------------------------------------------- +# The dump is a single-database dump (no CREATE DATABASE), so drop & recreate the +# database with the same charset/collation, then import. Database-level grants +# for the espocrm user survive a DROP DATABASE, so the app keeps its access. +echo "Recreating database '$DB_NAME' ..." +podman exec "$DB_CTR" sh -c \ + 'exec mariadb -uroot -p"$MARIADB_ROOT_PASSWORD" -e "DROP DATABASE IF EXISTS '"$DB_NAME"'; CREATE DATABASE '"$DB_NAME"' CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"' +echo "Recreated database (rc=$?)" + +echo "Importing DB dump from $DB_BAK ..." +gunzip -c "$DB_BAK" | podman exec -i "$DB_CTR" sh -c \ + 'exec mariadb -uroot -p"$MARIADB_ROOT_PASSWORD" '"$DB_NAME" +echo "Imported DB dump (rc=$?)" + +# --- Bring the app back up ---------------------------------------------------- +# Start the web container first; its entrypoint fixes ownership of the writable +# resources before the daemon (which shares the same data dir) starts. +systemctl --user start "$WEB_SVC" +echo "Started $WEB_SVC (rc=$?)" +echo "Waiting for EspoCRM to respond..." +for i in $(seq 1 90); do + code=$(curl -s -o /dev/null -w '%{http_code}' "${SITE_URL}/" 2>/dev/null) + case "$code" in 200|302) break ;; esac + sleep 2 +done +echo "EspoCRM responded with HTTP $code (rc=$?)" + +systemctl --user start "$DAEMON_SVC" +echo "Started $DAEMON_SVC (rc=$?)" + +# Clear cache / rebuild so the restored config and metadata take effect. +podman exec -u www-data "$WEB_CTR" php command.php rebuild +echo "Rebuilt EspoCRM cache (rc=$?)" + +echo "" +echo "Restore of set '$TS' complete." +echo "EspoCRM is reachable at ${SITE_URL}" +echo "Status: systemctl --user status $POD_SVC $DB_SVC $WEB_SVC $DAEMON_SVC"