#!/usr/bin/env bash set -Eeuo pipefail IFS=$'\n\t' umask 022 STEP_NAME="STEP032_2156_PRESENZE_DISTRIBUITE_LOCAL_GENERATOR_INSTALL_PATCH_VERIFIED_20260730" EXPECTED_HOST="ax-accessi" EXPECTED_IP="192.168.215.6" WORKER_PATH="/usr/local/sbin/dvm-presenze-portal-generator" SOURCE_ROOT="/var/www/html/presenze.distribuite" SOURCE_DB="presenze_distribuite" SOURCE_VHOST="/etc/apache2/sites-available/presenze.distribuite.axuno.it.conf" OUTPUT_DIR="$PWD" RUN_ID="$(date +%Y%m%d_%H%M%S)" WORKDIR="$(mktemp -d "/tmp/${STEP_NAME}.XXXXXX")" REPORT_DIR="${WORKDIR}/report" ZIP_PATH="${OUTPUT_DIR}/${STEP_NAME}_${RUN_ID}.zip" BACKUP_PATH="${WORKDIR}/worker.previous" INSTALLED_NEW=0 HAD_PREVIOUS=0 FINISHED=0 mkdir -p "$REPORT_DIR" log() { printf '%s\n' "$*" | tee -a "${REPORT_DIR}/run.log" } cleanup() { local rc=$? if [[ $rc -ne 0 && $FINISHED -eq 0 ]]; then if [[ $HAD_PREVIOUS -eq 1 && -f "$BACKUP_PATH" ]]; then install -o root -g root -m 0750 "$BACKUP_PATH" "$WORKER_PATH" || true elif [[ $INSTALLED_NEW -eq 1 ]]; then rm -f -- "$WORKER_PATH" || true fi fi rm -rf -- "$WORKDIR" exit "$rc" } trap cleanup EXIT trap 'log "ERRORE linea ${LINENO}: ${BASH_COMMAND}"' ERR if [[ ${EUID} -ne 0 ]]; then log "ERRORE: eseguire come root tramite sudo." exit 1 fi HOST_NOW="$(hostname -s 2>/dev/null || hostname)" IPS_NOW="$(hostname -I 2>/dev/null || true)" if [[ "$HOST_NOW" != "$EXPECTED_HOST" ]] || [[ " $IPS_NOW " != *" $EXPECTED_IP "* ]]; then log "ERRORE: server errato. Rilevato host=${HOST_NOW}, IP=${IPS_NOW:-N/D}; atteso ${EXPECTED_HOST} / ${EXPECTED_IP}." exit 1 fi for cmd in bash install rsync mysql mysqldump apache2ctl a2ensite a2dissite flock zip sha256sum readlink sed grep find mktemp; do command -v "$cmd" >/dev/null 2>&1 || { log "ERRORE: comando mancante: $cmd"; exit 1; } done [[ -d "$SOURCE_ROOT" ]] || { log "ERRORE: base assente: $SOURCE_ROOT"; exit 1; } [[ -f "$SOURCE_ROOT/phpgen_settings.php" ]] || { log "ERRORE: configurazione base assente: $SOURCE_ROOT/phpgen_settings.php"; exit 1; } [[ -f "$SOURCE_VHOST" ]] || { log "ERRORE: vhost base assente: $SOURCE_VHOST"; exit 1; } [[ -L "$SOURCE_ROOT/bower_components" ]] || { log "ERRORE: bower_components non è un symlink."; exit 1; } [[ -L "$SOURCE_ROOT/node_modules" ]] || { log "ERRORE: node_modules non è un symlink."; exit 1; } [[ -d "$(readlink -f "$SOURCE_ROOT/bower_components")" ]] || { log "ERRORE: target bower_components assente."; exit 1; } [[ -d "$(readlink -f "$SOURCE_ROOT/node_modules")" ]] || { log "ERRORE: target node_modules assente."; exit 1; } DB_FOUND="$(mysql --batch --skip-column-names -e "SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME='${SOURCE_DB}'" 2>/dev/null || true)" [[ "$DB_FOUND" == "$SOURCE_DB" ]] || { log "ERRORE: database base non accessibile: $SOURCE_DB"; exit 1; } if [[ -e "$WORKER_PATH" ]]; then cp -a -- "$WORKER_PATH" "$BACKUP_PATH" HAD_PREVIOUS=1 fi cat > "${WORKDIR}/dvm-presenze-portal-generator" <<'WORKER' #!/usr/bin/env bash set -Eeuo pipefail IFS=$'\n\t' umask 022 SOURCE_ROOT="${DVM_GEN_SOURCE_ROOT:-/var/www/html/presenze.distribuite}" SOURCE_DB="${DVM_GEN_SOURCE_DB:-presenze_distribuite}" SOURCE_DOMAIN="${DVM_GEN_SOURCE_DOMAIN:-presenze.distribuite.axuno.it}" WEB_ROOT="${DVM_GEN_WEB_ROOT:-/var/www/html}" SITES_AVAILABLE="${DVM_GEN_SITES_AVAILABLE:-/etc/apache2/sites-available}" SITES_ENABLED="${DVM_GEN_SITES_ENABLED:-/etc/apache2/sites-enabled}" SOURCE_VHOST="${DVM_GEN_SOURCE_VHOST:-${SITES_AVAILABLE}/${SOURCE_DOMAIN}.conf}" LOCK_FILE="${DVM_GEN_LOCK_FILE:-/var/lock/dvm-presenze-portal-generator.lock}" LOG_FILE="${DVM_GEN_LOG_FILE:-/var/log/dvm-presenze-portal-generator.log}" MYSQL_BIN="${DVM_GEN_MYSQL_BIN:-mysql}" MYSQLDUMP_BIN="${DVM_GEN_MYSQLDUMP_BIN:-mysqldump}" APACHECTL_BIN="${DVM_GEN_APACHECTL_BIN:-apache2ctl}" A2ENSITE_BIN="${DVM_GEN_A2ENSITE_BIN:-a2ensite}" A2DISSITE_BIN="${DVM_GEN_A2DISSITE_BIN:-a2dissite}" RSYNC_BIN="${DVM_GEN_RSYNC_BIN:-rsync}" EXPECTED_HOST="${DVM_GEN_EXPECTED_HOST:-ax-accessi}" EXPECTED_IP="${DVM_GEN_EXPECTED_IP:-192.168.215.6}" TEST_MODE="${DVM_GEN_TEST_MODE:-0}" COMMAND="${1:-}" SLUG="${2:-}" TARGET_DOMAIN="" TARGET_ROOT="" TARGET_DB="" TARGET_VHOST="" TARGET_VHOST_NAME="" STAGE_ROOT="" CREATED_DB=0 CREATED_ROOT=0 CREATED_VHOST=0 ENABLED_SITE=0 IN_ROLLBACK=0 usage() { cat <<'USAGE' Uso: dvm-presenze-portal-generator inspect dvm-presenze-portal-generator plan dvm-presenze-portal-generator create Regole slug: lettere minuscole, numeri e trattini; deve iniziare e finire con lettera o numero. Nomi generati: dominio: presenze..axuno.it cartella: /var/www/html/presenze. database: presenze_ USAGE } emit() { printf '%s=%s\n' "$1" "$2" } log_event() { local message="$1" if [[ "$TEST_MODE" == "1" ]]; then printf '%s %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$message" >> "$LOG_FILE" else printf '%s %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$message" >> "$LOG_FILE" chown root:root "$LOG_FILE" 2>/dev/null || true chmod 0640 "$LOG_FILE" 2>/dev/null || true fi } validate_server() { [[ "$TEST_MODE" == "1" ]] && return 0 local host_now ips_now host_now="$(hostname -s 2>/dev/null || hostname)" ips_now="$(hostname -I 2>/dev/null || true)" [[ "$host_now" == "$EXPECTED_HOST" ]] || { echo "ERRORE: host errato: $host_now" >&2; exit 20; } [[ " $ips_now " == *" $EXPECTED_IP "* ]] || { echo "ERRORE: IP atteso non presente: $ips_now" >&2; exit 21; } } validate_slug() { [[ "$SLUG" =~ ^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$ ]] || { echo "ERRORE: slug non valido: $SLUG" >&2 exit 22 } case "$SLUG" in distribuite|distro|silvelox|dvmservice|webservice) echo "ERRORE: slug riservato: $SLUG" >&2 exit 23 ;; esac } build_names() { local db_suffix db_suffix="${SLUG//-/_}" TARGET_DOMAIN="presenze.${SLUG}.axuno.it" TARGET_ROOT="${WEB_ROOT}/presenze.${SLUG}" TARGET_DB="presenze_${db_suffix}" TARGET_VHOST_NAME="${TARGET_DOMAIN}.conf" TARGET_VHOST="${SITES_AVAILABLE}/${TARGET_VHOST_NAME}" } mysql_scalar() { "$MYSQL_BIN" --batch --skip-column-names -e "$1" } database_exists() { local db="$1" result result="$(mysql_scalar "SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME='${db}'" 2>/dev/null || true)" [[ "$result" == "$db" ]] } check_source() { [[ -d "$SOURCE_ROOT" ]] || { echo "ERRORE: base assente: $SOURCE_ROOT" >&2; exit 30; } [[ -f "$SOURCE_ROOT/phpgen_settings.php" ]] || { echo "ERRORE: phpgen_settings.php assente" >&2; exit 31; } [[ -f "$SOURCE_VHOST" ]] || { echo "ERRORE: vhost sorgente assente: $SOURCE_VHOST" >&2; exit 32; } [[ -L "$SOURCE_ROOT/bower_components" ]] || { echo "ERRORE: symlink bower_components assente" >&2; exit 33; } [[ -L "$SOURCE_ROOT/node_modules" ]] || { echo "ERRORE: symlink node_modules assente" >&2; exit 34; } [[ -d "$(readlink -f "$SOURCE_ROOT/bower_components")" ]] || { echo "ERRORE: target bower_components interrotto" >&2; exit 35; } [[ -d "$(readlink -f "$SOURCE_ROOT/node_modules")" ]] || { echo "ERRORE: target node_modules interrotto" >&2; exit 36; } database_exists "$SOURCE_DB" || { echo "ERRORE: database sorgente assente: $SOURCE_DB" >&2; exit 37; } grep -Fq "'database' => '${SOURCE_DB}'" "$SOURCE_ROOT/phpgen_settings.php" || { echo "ERRORE: riferimento database sorgente non trovato in phpgen_settings.php" >&2 exit 38 } grep -Fq "$SOURCE_DOMAIN" "$SOURCE_VHOST" || { echo "ERRORE: dominio sorgente non trovato nel vhost" >&2; exit 39; } grep -Fq "$SOURCE_ROOT" "$SOURCE_VHOST" || { echo "ERRORE: DocumentRoot sorgente non trovato nel vhost" >&2; exit 40; } } check_target_absent() { [[ ! -e "$TARGET_ROOT" ]] || { echo "ERRORE: cartella già esistente: $TARGET_ROOT" >&2; exit 41; } [[ ! -e "$TARGET_VHOST" ]] || { echo "ERRORE: vhost già esistente: $TARGET_VHOST" >&2; exit 42; } [[ ! -e "$SITES_ENABLED/$TARGET_VHOST_NAME" ]] || { echo "ERRORE: sito già abilitato: $TARGET_VHOST_NAME" >&2; exit 43; } ! database_exists "$TARGET_DB" || { echo "ERRORE: database già esistente: $TARGET_DB" >&2; exit 44; } } clear_runtime_dir() { local root="$1" local relative="$2" local dir="${root}/${relative}" [[ -d "$dir" ]] || return 0 find "$dir" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + } patch_database_name() { local file="$1" count count="$(grep -F -c "'database' => '${SOURCE_DB}'" "$file" || true)" [[ "$count" == "1" ]] || { echo "ERRORE: atteso un solo riferimento a ${SOURCE_DB} in ${file}, trovati ${count}" >&2 return 1 } sed -i "s/'database' => '${SOURCE_DB}'/'database' => '${TARGET_DB}'/" "$file" grep -Fq "'database' => '${TARGET_DB}'" "$file" ! grep -Fq "'database' => '${SOURCE_DB}'" "$file" } render_vhost() { local destination="$1" cp -- "$SOURCE_VHOST" "$destination" sed -i \ -e "s#${SOURCE_DOMAIN}#${TARGET_DOMAIN}#g" \ -e "s#${SOURCE_ROOT}#${TARGET_ROOT}#g" \ "$destination" grep -Fq "ServerName ${TARGET_DOMAIN}" "$destination" grep -Fq "DocumentRoot ${TARGET_ROOT}" "$destination" grep -Fq "" "$destination" ! grep -Fq "$SOURCE_DOMAIN" "$destination" ! grep -Fq "$SOURCE_ROOT" "$destination" } rollback() { local rc=$? [[ "$COMMAND" == "create" ]] || exit "$rc" [[ $rc -ne 0 ]] || exit 0 [[ $IN_ROLLBACK -eq 0 ]] || exit "$rc" IN_ROLLBACK=1 trap - ERR echo "ROLLBACK: ripristino dopo errore." >&2 if [[ $ENABLED_SITE -eq 1 ]]; then "$A2DISSITE_BIN" "$TARGET_VHOST_NAME" >/dev/null 2>&1 || true fi if [[ $CREATED_VHOST -eq 1 ]]; then rm -f -- "$TARGET_VHOST" "$SITES_ENABLED/$TARGET_VHOST_NAME" || true fi if [[ $CREATED_ROOT -eq 1 ]]; then rm -rf --one-file-system -- "$TARGET_ROOT" || true fi if [[ -n "$STAGE_ROOT" && -e "$STAGE_ROOT" ]]; then rm -rf --one-file-system -- "$STAGE_ROOT" || true fi if [[ $CREATED_DB -eq 1 ]]; then "$MYSQL_BIN" -e "DROP DATABASE IF EXISTS \`${TARGET_DB}\`" >/dev/null 2>&1 || true fi "$APACHECTL_BIN" configtest >/dev/null 2>&1 || true "$APACHECTL_BIN" graceful >/dev/null 2>&1 || true log_event "FAILED slug=${SLUG} rollback=completed exit=${rc}" exit "$rc" } trap rollback EXIT trap 'echo "ERRORE linea ${LINENO}: ${BASH_COMMAND}" >&2' ERR inspect_command() { check_source emit status OK emit source_root "$SOURCE_ROOT" emit source_database "$SOURCE_DB" emit source_domain "$SOURCE_DOMAIN" emit bower_target "$(readlink -f "$SOURCE_ROOT/bower_components")" emit node_target "$(readlink -f "$SOURCE_ROOT/node_modules")" } plan_command() { validate_slug build_names check_source check_target_absent emit status READY emit slug "$SLUG" emit domain "$TARGET_DOMAIN" emit document_root "$TARGET_ROOT" emit database "$TARGET_DB" emit vhost "$TARGET_VHOST" } create_command() { [[ ${EUID} -eq 0 ]] || { echo "ERRORE: create richiede root." >&2; exit 50; } validate_slug build_names check_source check_target_absent mkdir -p "$(dirname "$LOCK_FILE")" "$(dirname "$LOG_FILE")" exec 9>"$LOCK_FILE" flock -n 9 || { echo "ERRORE: un'altra generazione è in corso." >&2; exit 51; } STAGE_ROOT="$(mktemp -d "${WEB_ROOT}/.dvm-generator.${SLUG}.XXXXXX")" log_event "START slug=${SLUG} domain=${TARGET_DOMAIN} database=${TARGET_DB}" "$RSYNC_BIN" -aH \ --exclude='/bower_components' \ --exclude='/node_modules' \ "$SOURCE_ROOT/" "$STAGE_ROOT/" rm -rf -- "$STAGE_ROOT/bower_components" "$STAGE_ROOT/node_modules" ln -s "$(readlink "$SOURCE_ROOT/bower_components")" "$STAGE_ROOT/bower_components" ln -s "$(readlink "$SOURCE_ROOT/node_modules")" "$STAGE_ROOT/node_modules" while IFS= read -r runtime_dir; do clear_runtime_dir "$STAGE_ROOT" "$runtime_dir" done <<'RUNTIME_DIRS' templates_c log libs/stimulsoft_legacy/tmp libs/mpdf/tmp libs/pdo/classes/library/tcpdf/cache theme/php/uploads webservices_report/log webServiceTime/classes/library/tcpdf/cache components/templates/custom_templates/ax_attendance/ax_schede_presenze_modA/timeCtrl_lastonsilvelox/log components/templates/custom_templates/ax_attendance/ax_schede_presenze_modA/timeCtrl_silvelox_ok/log components/templates/custom_templates/ax_attendance/ax_time_controll/timeCtrl/log components/templates/custom_templates/ax_attendance/impostazioni-generali/ctrl/upload RUNTIME_DIRS patch_database_name "$STAGE_ROOT/phpgen_settings.php" "$MYSQL_BIN" -e "CREATE DATABASE \`${TARGET_DB}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci" CREATED_DB=1 "$MYSQLDUMP_BIN" --default-character-set=utf8mb4 --routines --events --triggers --hex-blob "$SOURCE_DB" \ | "$MYSQL_BIN" --default-character-set=utf8mb4 "$TARGET_DB" mv -- "$STAGE_ROOT" "$TARGET_ROOT" STAGE_ROOT="" CREATED_ROOT=1 render_vhost "$TARGET_VHOST" chown root:root "$TARGET_VHOST" chmod 0644 "$TARGET_VHOST" CREATED_VHOST=1 "$A2ENSITE_BIN" "$TARGET_VHOST_NAME" >/dev/null ENABLED_SITE=1 "$APACHECTL_BIN" configtest "$APACHECTL_BIN" graceful [[ -d "$TARGET_ROOT" ]] [[ -L "$TARGET_ROOT/bower_components" ]] [[ -L "$TARGET_ROOT/node_modules" ]] [[ -f "$TARGET_VHOST" ]] database_exists "$TARGET_DB" emit status CREATED emit slug "$SLUG" emit domain "$TARGET_DOMAIN" emit document_root "$TARGET_ROOT" emit database "$TARGET_DB" emit vhost "$TARGET_VHOST" log_event "SUCCESS slug=${SLUG} domain=${TARGET_DOMAIN} database=${TARGET_DB}" } case "$COMMAND" in -h|--help|help|'') usage exit 0 ;; inspect) validate_server inspect_command ;; plan) validate_server plan_command ;; create) validate_server create_command ;; *) echo "ERRORE: comando sconosciuto: $COMMAND" >&2 usage >&2 exit 2 ;; esac WORKER bash -n "${WORKDIR}/dvm-presenze-portal-generator" install -o root -g root -m 0750 "${WORKDIR}/dvm-presenze-portal-generator" "$WORKER_PATH" INSTALLED_NEW=1 bash -n "$WORKER_PATH" { echo "STEP: $STEP_NAME" echo "Server: $HOST_NOW" echo "IP: $IPS_NOW" echo "Worker installato: $WORKER_PATH" echo "Modalità: installazione motore locale; nessun portale creato" echo "Base: $SOURCE_ROOT" echo "Database base: $SOURCE_DB" echo "Vhost base: $SOURCE_VHOST" echo "Hash worker: $(sha256sum "$WORKER_PATH" | awk '{print $1}')" } > "${REPORT_DIR}/01_INSTALLATION.txt" "$WORKER_PATH" inspect > "${REPORT_DIR}/02_WORKER_INSPECT.txt" "$WORKER_PATH" plan generator-verifica > "${REPORT_DIR}/03_DRY_PLAN.txt" "$WORKER_PATH" --help > "${REPORT_DIR}/04_HELP.txt" { echo "Verifica sintassi script installazione: OK" echo "Verifica sintassi worker installato: OK" echo "Verifica sorgente/database/vhost: OK" echo "Verifica symlink condivisi: OK" echo "Piano simulato generator-verifica: OK" echo "Portali creati: 0" echo "Database creati: 0" echo "Vhost creati/abilitati: 0" echo "Apache ricaricato: NO" echo "Silvelox toccato: NO" } > "${REPORT_DIR}/99_SUMMARY.txt" cp -- "$WORKER_PATH" "${REPORT_DIR}/dvm-presenze-portal-generator.txt" ( cd "$REPORT_DIR" zip -q -r "$ZIP_PATH" . ) zip -T "$ZIP_PATH" > "${REPORT_DIR}/zip_test.tmp" rm -f "${REPORT_DIR}/zip_test.tmp" FINISHED=1 log "INSTALLAZIONE COMPLETATA" log "Worker: $WORKER_PATH" log "Nessun portale creato." log "Output: $ZIP_PATH"