From 71b172aeac1cade5c368a0b161faf5313a32b042 Mon Sep 17 00:00:00 2001 From: Anthony Date: Wed, 13 Mar 2024 21:25:45 +0800 Subject: [PATCH] Revised backup logic --- manifest.jps | 50 ++-- scripts/backup-logic.sh | 218 +++++++++++++++ scripts/backup-main.js | 4 +- scripts/check_app.sh.sh | 11 - scripts/mb-backbackup-manager.js | 89 ------ scripts/mb-backup-manager.js | 454 +++++++++++++++++++++++++++++++ 6 files changed, 697 insertions(+), 129 deletions(-) create mode 100644 scripts/backup-logic.sh delete mode 100644 scripts/check_app.sh.sh delete mode 100644 scripts/mb-backbackup-manager.js create mode 100644 scripts/mb-backup-manager.js diff --git a/manifest.jps b/manifest.jps index ec629cb..a42d908 100644 --- a/manifest.jps +++ b/manifest.jps @@ -19,7 +19,15 @@ onInstall: - checkApplication - checkAddons - installRestic - - setSchedule + +menu: + confirmText: Backup ALL Now? + loadingText: Backing up ALL data on your site + action: backupnow + caption: Backup All + successText: Backup All Completed Successfully + title: Backup All Now + submitButtonText: Backup Now onUninstall: - callScript: uninstall @@ -40,8 +48,6 @@ onAfterClone: cronTime: "0 * * * *" backupCount: "5" -onAfterConfirmTransfer: setSchedule - actions: checkApplication: - cmd[${nodes.cp.master.id}]: |- @@ -53,7 +59,9 @@ actions: - stopEvent: type: warning message: Deployed application is not supported by Backup add-on. - + backupnow: + message: BACKUP ALL DONE + checkAddons: - script: |- var onAfterReturn = { setGlobals: {} }, @@ -89,7 +97,14 @@ actions: yum-config-manager --enable copr:copr.fedorainfracloud.org:copart:restic yum -y install restic yum-config-manager --disable copr:copr.fedorainfracloud.org:copart:restic - wget -O /etc/logrotate.d/backup-addon ${baseUrl}/scripts/backup-addon; + echo "/var/log/backup_addon.log { + weekly + rotate 52 + missingok + notifempty + compress + copytruncate + }" > /etc/logrotate.d/backup-addon fi user: root @@ -100,13 +115,10 @@ actions: params: scriptName: ${env.envName}-${globals.scriptSufix} baseUrl: ${baseUrl} - cronTime: '${this.cronTime}' - backupCount: ${this.backupCount} + cronTime: '0 * * * *' + backupCount: '5' userId: ${env.uid} - storageNodeId: ${response.storageCtid} - backupExecNode: ${nodes.cp.master.id} - storageEnv: ${response.storageEnvShortName} - + callScript: script: |- var resp = api.dev.scripting.Eval(appid, session, '${env.envName}-${globals.scriptSufix}', {action:"${this}"}); @@ -133,18 +145,4 @@ actions: echo "${settings.backupedEnvName}" > /root/.backupedenv echo "${settings.backupDir}" > /root/.backupid user: root - - callScript: restore - - configure: - - setSchedule - - getStorageCtid: - - script: scripts/getStorageCtid.js - - setSchedule: - - setGlobals: - cron: ${settings.cronTime} - - installScript: - cronTime: ${globals.cron} - backupCount: 5 - \ No newline at end of file + - callScript: restore \ No newline at end of file diff --git a/scripts/backup-logic.sh b/scripts/backup-logic.sh new file mode 100644 index 0000000..789d7d7 --- /dev/null +++ b/scripts/backup-logic.sh @@ -0,0 +1,218 @@ +#!/bin/bash + +BASE_URL=$2 +BACKUP_TYPE=$3 +BACKUP_LOG_FILE=$4 +ENV_NAME=$5 +BACKUP_COUNT=$6 +APP_PATH=$7 +USER_SESSION=$8 +USER_EMAIL=$9 +BACKUP_PATH=${10} + +# Ensure restic is installed +if ! which restic &>/dev/null; then + if which dnf &>/dev/null; then + dnf install -y epel-release + dnf install -y restic + elif which yum &>/dev/null; then + yum-config-manager --add-repo https://copr.fedorainfracloud.org/coprs/copart/restic/repo/epel-7/copart-restic-epel-7.repo + yum-config-manager --enable copart-restic + yum -y install restic + yum-config-manager --disable copart-restic + fi +fi + +# Function definitions remain the same, but adjustments are made to use $BACKUP_PATH +function update_restic(){ + restic self-update 2>&1; +} + +function check_backup_repo(){ + local backup_repo_path="/mnt/backups/${ENV_NAME}" + [ -d "${backup_repo_path}" ] || mkdir -p "${backup_repo_path}" + export FILES_COUNT=$(find "${backup_repo_path}" -mindepth 1 | wc -l) + + if [ "${FILES_COUNT}" -gt 0 ]; then + echo "$(date) ${ENV_NAME} Checking the backup repository integrity and consistency" | tee -a "${BACKUP_LOG_FILE}" + + if [[ $(ls -A "${backup_repo_path}/locks") ]]; then + echo "$(date) ${ENV_NAME} Backup repository has a stale lock, removing" | tee -a "${BACKUP_LOG_FILE}" + GOGC=20 RESTIC_PASSWORD="${ENV_NAME}" restic -r "${backup_repo_path}" unlock + sendEmailNotification + fi + + if ! GOGC=20 RESTIC_PASSWORD="${ENV_NAME}" restic -q -r "${backup_repo_path}" check --read-data-subset=5%; then + echo "Backup repository integrity check failed." | tee -a "${BACKUP_LOG_FILE}" + exit 1 + fi + else + echo "$(date) ${ENV_NAME} Initializing new backup repository" | tee -a "${BACKUP_LOG_FILE}" + GOGC=20 RESTIC_PASSWORD="${ENV_NAME}" restic init -r "${backup_repo_path}" + fi +} + +function sendEmailNotification() { + if [ -e "/usr/lib/jelastic/modules/api.module" ]; then + [ -e "/var/run/jem.pid" ] && return 0; + CURRENT_PLATFORM_MAJOR_VERSION=$(jem api apicall -s --connect-timeout 3 --max-time 15 [API_DOMAIN]/1.0/statistic/system/rest/getversion 2>/dev/null | jq .version | grep -o [0-9.]* | awk -F . '{print $1}') + if [ "${CURRENT_PLATFORM_MAJOR_VERSION}" -ge "7" ]; then + echo "$(date) ${ENV_NAME} Sending e-mail notification about removing the stale lock" | tee -a "$BACKUP_LOG_FILE" + SUBJECT="Stale lock is removed on ${BACKUP_PATH}/${ENV_NAME} backup repo" + BODY="Please pay attention to ${BACKUP_PATH}/${ENV_NAME} backup repo because the stale lock left from previous operation is removed during the integrity check and backup rotation. Manual check of backup repo integrity and consistency is highly desired." + jem api apicall -s --connect-timeout 3 --max-time 15 [API_DOMAIN]/1.0/message/email/rest/send --data-urlencode "session=$USER_SESSION" --data-urlencode "to=$USER_EMAIL" --data-urlencode "subject=$SUBJECT" --data-urlencode "body=$BODY" + if [[ $? != 0 ]]; then + echo "$(date) ${ENV_NAME} Sending of e-mail notification failed" | tee -a "$BACKUP_LOG_FILE" + else + echo "$(date) ${ENV_NAME} E-mail notification is sent successfully" | tee -a "$BACKUP_LOG_FILE" + fi + elif [ -z "${CURRENT_PLATFORM_MAJOR_VERSION}" ]; then + echo "$(date) ${ENV_NAME} Error when checking the platform version" | tee -a "$BACKUP_LOG_FILE" + else + echo "$(date) ${ENV_NAME} Email notification is not sent because this functionality is unavailable for current platform version." | tee -a "$BACKUP_LOG_FILE" + fi + else + echo "$(date) ${ENV_NAME} Email notification is not sent because this functionality is unavailable for current platform version." | tee -a "$BACKUP_LOG_FILE" + fi +} + +function rotate_snapshots(){ + local backup_repo_path="/mnt/backups/${ENV_NAME}" + echo "$(date) ${ENV_NAME} Rotating snapshots by keeping the last ${BACKUP_COUNT}" | tee -a "${BACKUP_LOG_FILE}" + + if [[ $(ls -A "${backup_repo_path}/locks") ]]; then + echo "$(date) ${ENV_NAME} Backup repository has a stale lock, removing" | tee -a "${BACKUP_LOG_FILE}" + GOGC=20 RESTIC_PASSWORD="${ENV_NAME}" restic -r "${backup_repo_path}" unlock + sendEmailNotification + fi + + if ! GOGC=20 RESTIC_COMPRESSION=off RESTIC_PACK_SIZE=8 RESTIC_PASSWORD="${ENV_NAME}" restic forget -q -r "${backup_repo_path}" --keep-last "${BACKUP_COUNT}" --prune | tee -a "${BACKUP_LOG_FILE}"; then + echo "Backup rotation failed." | tee -a "${BACKUP_LOG_FILE}" + exit 1 + fi +} + +function create_snapshot(){ + local backup_repo_path="/mnt/backups/${ENV_NAME}" + DUMP_NAME=$(date "+%F_%H%M%S_%Z") + echo "$(date) ${ENV_NAME} Begin uploading the ${DUMP_NAME} snapshot to backup storage" | tee -a "${BACKUP_LOG_FILE}" + + if ! GOGC=20 RESTIC_COMPRESSION=off RESTIC_PACK_SIZE=8 RESTIC_READ_CONCURRENCY=8 RESTIC_PASSWORD="${ENV_NAME}" restic backup -q -r "${backup_repo_path}" --tag "${DUMP_NAME} ${BACKUP_ADDON_COMMIT_ID} ${BACKUP_TYPE}" "${APP_PATH}" ~/wp_db_backup.sql | tee -a "${BACKUP_LOG_FILE}"; then + echo "Backup snapshot creation failed." | tee -a "${BACKUP_LOG_FILE}" + exit 1 + fi + + echo "$(date) ${ENV_NAME} End uploading the ${DUMP_NAME} snapshot to backup storage" | tee -a "${BACKUP_LOG_FILE}" +} + +function backup(){ + local backup_repo_path="/mnt/backups/${ENV_NAME}" + echo $$ > "/var/run/${ENV_NAME}_backup.pid" + BACKUP_ADDON_REPO=$(echo "${BASE_URL}" | sed 's|https://raw.githubusercontent.com/||' | awk -F '/' '{print $1"/"$2}') + BACKUP_ADDON_BRANCH=$(echo "${BASE_URL}" | sed 's|https://raw.githubusercontent.com/||' | awk -F '/' '{print $3}') + BACKUP_ADDON_COMMIT_ID=$(git ls-remote "https://github.com/${BACKUP_ADDON_REPO}.git" | grep "/${BACKUP_ADDON_BRANCH}$" | awk '{print $1}') + echo "$(date) ${ENV_NAME} Creating the ${BACKUP_TYPE} backup (using the backup addon with commit id ${BACKUP_ADDON_COMMIT_ID})" | tee -a "${BACKUP_LOG_FILE}" + + # Database credentials and dump + source /var/www/webroot/ROOT/wp-config.php + DB_HOST=$(echo "${DB_HOST}" | awk -F ':' '{print $1}') + DB_PORT=$(echo "${DB_HOST}" | awk -F ':' '{print $2:-3306}') + + echo "$(date) ${ENV_NAME} Creating the DB dump" | tee -a "${BACKUP_LOG_FILE}" + if ! mysqldump -h "${DB_HOST}" -P "${DB_PORT}" -u "${DB_USER}" -p"${DB_PASSWORD}" "${DB_NAME}" --force --single-transaction --quote-names --opt --databases > "${backup_repo_path}/wp_db_backup.sql"; then + echo "$(date) ${ENV_NAME} DB backup process failed." | tee -a "${BACKUP_LOG_FILE}" + exit 1 + fi + + # Backup process + if ! restic -r "${backup_repo_path}" backup "${backup_repo_path}/wp_db_backup.sql" "${APP_PATH}" --tag "${BACKUP_TYPE}" --host "${ENV_NAME}"; then + echo "$(date) ${ENV_NAME} Backup process failed." | tee -a "${BACKUP_LOG_FILE}" + exit 1 + fi + + echo "$(date) ${ENV_NAME} Backup process completed successfully." | tee -a "${BACKUP_LOG_FILE}" + rm -f "/var/run/${ENV_NAME}_backup.pid" +} + +function backup_wp_core() { + echo "Starting backup of WordPress core files excluding uploads directory..." + # Define the path to the WordPress installation and the backup tag + local wp_path="$APP_PATH" + local backup_tag="wp-core-$(date "+%F_%H%M%S_%Z")" + + # Exclude the uploads directory using the --exclude option + GOGC=20 RESTIC_PASSWORD="$RESTIC_PASSWORD" restic -r "$backupPath" backup \ + --tag "$backup_tag" \ + --exclude="$wp_path/wp-content/uploads" \ + "$wp_path" + + echo "WordPress core files backup completed." +} + +function backup_uploads() { + echo "Starting backup of WordPress uploads directory..." + # Define the path to the uploads directory and the backup tag + local uploads_path="$APP_PATH/wp-content/uploads" + local backup_tag="wp-uploads-$(date "+%F_%H%M%S_%Z")" + + # Perform the backup + GOGC=20 RESTIC_PASSWORD="$RESTIC_PASSWORD" restic -r "$backupPath" backup \ + --tag "$backup_tag" \ + "$uploads_path" + + echo "WordPress uploads directory backup completed." +} + +function backup_database() { + echo "Starting backup of WordPress database..." + # Define the backup tag and the path for the temporary SQL dump + local backup_tag="wp-db-$(date "+%F_%H%M%S_%Z")" + local sql_dump_path="/tmp/wp_db_backup.sql" + + # Create a database dump + mysqldump -h "$DB_HOST" -u "$DB_USER" -p"$DB_PASSWORD" "$DB_NAME" > "$sql_dump_path" + + # Perform the backup + GOGC=20 RESTIC_PASSWORD="$RESTIC_PASSWORD" restic -r "$backupPath" backup \ + --tag "$backup_tag" \ + "$sql_dump_path" + + # Remove the temporary SQL dump file + rm -f "$sql_dump_path" + echo "WordPress database backup completed." +} + +case "$1" in + backup) + backup_wp_core + backup_uploads + backup_database + ;; + backup_wp_core) + backup_wp_core + ;; + backup_uploads) + backup_uploads + ;; + backup_database) + backup_database + ;; + check_backup_repo) + check_backup_repo + ;; + rotate_snapshots) + rotate_snapshots + ;; + create_snapshot) + create_snapshot + ;; + update_restic) + update_restic + ;; + *) + echo "Usage: $0 {backup|backup_wp_core|backup_uploads|backup_database|check_backup_repo|rotate_snapshots|create_snapshot|update_restic}" + exit 2 +esac + + +exit $? \ No newline at end of file diff --git a/scripts/backup-main.js b/scripts/backup-main.js index 32633a4..e1b3fde 100644 --- a/scripts/backup-main.js +++ b/scripts/backup-main.js @@ -12,10 +12,8 @@ function run() { scriptName : "${scriptName}", envName : "${envName}", envAppid : "${envAppid}", - storageNodeId : "${storageNodeId}", - backupExecNode : "${backupExecNode}", backupCount : "${backupCount}", - storageEnv : "${storageEnv}" + backupPath : "/mnt/backup" }); api.local.ReturnResult( diff --git a/scripts/check_app.sh.sh b/scripts/check_app.sh.sh deleted file mode 100644 index 303ed55..0000000 --- a/scripts/check_app.sh.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -if [ -e /var/www/webroot/ROOT/wp-config.php ]; then - echo "$(date) trying to install the backup add-on" >> /var/log/backup_addon.log - if [ -e /home/jelastic/bin/wp ]; then - /home/jelastic/bin/wp --info >> /var/log/backup_addon.log - fi -else - echo "$(date) The application deployed to WEBROOT cannot be backuped by Jelastic backup add-on" >> /var/log/backup_addon.log - echo "Non-supported" -fi \ No newline at end of file diff --git a/scripts/mb-backbackup-manager.js b/scripts/mb-backbackup-manager.js deleted file mode 100644 index d6b3e63..0000000 --- a/scripts/mb-backbackup-manager.js +++ /dev/null @@ -1,89 +0,0 @@ -function BackupManager(config) { - var Response = com.hivext.api.Response, - Transport = com.hivext.api.core.utils.Transport, - Logger = org.slf4j.LoggerFactory.getLogger("backup-addon:" + config.envName); - - var me = this; - var BACKUP_TYPES = { - CORE: "core", - MEDIA: "media", - DATABASE: "db" - }; - - me.invoke = function(action) { - var actions = { - "backup": me.backup, - "restore": me.restore - }; - - return actions[action] ? actions[action].call(me) : { - result: Response.ERROR_UNKNOWN, - error: "Unknown action: " + action - }; - }; - - me.backup = function() { - // Sequentially perform all backups - me.backupCoreFiles(); - me.backupMediaFiles(); - me.backupDatabase(); - Logger.info("All backups completed successfully."); - }; - - me.backupCoreFiles = function() { - var backupName = me.getBackupName(BACKUP_TYPES.CORE); - // Placeholder for core files backup logic - Logger.info("Core files backup completed: " + backupName); - }; - - me.backupMediaFiles = function() { - var backupName = me.getBackupName(BACKUP_TYPES.ME DIA); - // Placeholder for media files backup logic - Logger.info("Media files backup completed: " + backupName); - }; - - me.backupDatabase = function() { - var backupName = me.getBackupName(BACKUP_TYPES.DATABASE); - // Placeholder for database backup logic - Logger.info("Database backup completed: " + backupName); - }; - - me.restore = function(backupName) { - // Determine type from backupName and call the respective restore function - var type = backupName.split("_")[1]; // Assumes naming convention is used - switch(type) { - case BACKUP_TYPES.CORE: - me.restoreCoreFiles(backupName); - break; - case BACKUP_TYPES.MEDIA: - me.restoreMediaFiles(backupName); - break; - case BACKUP_TYPES.DATABASE: - me.restoreDatabase(backupName); - break; - default: - Logger.error("Unknown backup type for restoration: " + backupName); - } - }; - - me.restoreCoreFiles = function(backupName) { - // Placeholder for core files restore logic - Logger.info("Core files restoration completed: " + backupName); - }; - - me.restoreMediaFiles = function(backupName) { - // Placeholder for media files restore logic - Logger.info("Media files restoration completed: " + backupName); - }; - - me.restoreDatabase = function(backupName) { - // Placeholder for database restore logic - Logger.info("Database restoration completed: " + backupName); - }; - - me.getBackupName = function(type) { - var dateFormat = new java.text.SimpleDateFormat("yyyyMMddHHmmss"); - var dateStr = dateFormat.format(new java.util.Date()); - return config.envName + "_" + type + "_" + dateStr; - }; -} diff --git a/scripts/mb-backup-manager.js b/scripts/mb-backup-manager.js new file mode 100644 index 0000000..26cf222 --- /dev/null +++ b/scripts/mb-backup-manager.js @@ -0,0 +1,454 @@ +function BackupManager(config) { + /** + * Implements backup management of the environment data + * @param {{ + * session : {String} + * baseUrl : {String} + * uid : {Number} + * cronTime : {String} + * scriptName : {String} + * envName : {String} + * envAppid : {String} + * [backupCount] : {String} + * }} config + * @constructor + */ + + var Response = com.hivext.api.Response, + EnvironmentResponse = com.hivext.api.environment.response.EnvironmentResponse, + ScriptEvalResponse = com.hivext.api.development.response.ScriptEvalResponse, + Transport = com.hivext.api.core.utils.Transport, + Random = com.hivext.api.utils.Random, + SimpleDateFormat = java.text.SimpleDateFormat, + StrSubstitutor = org.apache.commons.lang3.text.StrSubstitutor, + Scripting = com.hivext.api.development.Scripting, + LoggerFactory = org.slf4j.LoggerFactory, + LoggerName = "scripting.logger.backup-addon:" + config.envName, + Logger = LoggerFactory.getLogger(LoggerName), + + me = this, + nodeManager, session; + + config = config || {}; + session = config.session; + nodeManager = new NodeManager(config.envName); + + me.invoke = function (action) { + var actions = { + "install": me.install, + "uninstall": me.uninstall, + "backup": me.backup, + "restore": me.restore, + "backup_wp_core": me.backup_wp_core, + "backup_uploads": me.backup_uploads, + "backup_database": me.backup_database + }; + + if (!actions[action]) { + return { + result: Response.ERROR_UNKNOWN, + error: "unknown action [" + action + "]" + } + } + + return actions[action].call(me); + }; + + me.install = function () { + var resp; + + return me.exec([ + [me.cmd, ['echo $(date) %(envName) "Creating the backup task for %(envName) with the backup count %(backupCount), backup schedule %(cronTime)" | tee -a %(backupLogFile)'], + { + envName: config.envName, + cronTime: config.cronTime, + backupCount: config.backupCount, + backupLogFile: "/var/log/backup_addon.log" + }], + [me.createScript], + [me.clearScheduledBackups], + [me.scheduleBackup] + ]); + }; + + me.uninstall = function () { + return me.exec(me.clearScheduledBackups); + }; + + me.checkCurrentlyRunningBackup = function () { + var resp = me.exec([ + [me.cmd, ['pgrep -f "%(envName)"_backup-logic.sh 1>/dev/null && echo "Running"; true'], + { + envName: config.envName + }] + ]); + if (resp.responses[0].out == "Running") { + return { + result: Response.ERROR_UNKNOWN, + error: "Another backup process is already running" + } + } else { + return { + "result": 0 + }; + } + } + + me.backup = function () { + var backupType = !getParam("task") ? "manual" : "auto"; + + var backupCallParams = { + envName: config.envName, + appPath: "/var/www/webroot/ROOT", + backupCount: config.backupCount, + backupLogFile: "/var/log/backup_addon.log", + baseUrl: config.baseUrl, + backupType: backupType, + session: session, + email: user.email, + backupPath: "/mnt/backup" // Direct path to mounted backup storage + }; + + return me.exec([ + [me.checkEnvStatus], + [me.checkCurrentlyRunningBackup], + [me.cmd, [ + '[ -f /root/%(envName)_backup-logic.sh ] && rm -f /root/%(envName)_backup-logic.sh || true', + 'wget -O /root/%(envName)_backup-logic.sh %(baseUrl)/scripts/backup-logic.sh' + ], { + nodeGroup: "cp", + envName: config.envName, + baseUrl: config.baseUrl + }], + [me.cmd, [ + 'bash /root/%(envName)_backup-logic.sh update_restic' + ], { + nodeGroup: "cp", + envName: config.envName + } ], + [me.cmd, [ + 'bash /root/%(envName)_backup-logic.sh check_backup_repo %(baseUrl) %(backupType) %(backupPath) %(backupLogFile) %(envName) %(backupCount) %(appPath) %(session) %(email)' + ], backupCallParams ], + [me.cmd, [ + 'bash /root/%(envName)_backup-logic.sh backup %(baseUrl) %(backupType) %(backupPath) %(backupLogFile) %(envName) %(backupCount) %(appPath)' + ], backupCallParams ], + [me.cmd, [ + 'bash /root/%(envName)_backup-logic.sh create_snapshot %(baseUrl) %(backupType) %(backupPath) %(backupLogFile) %(envName) %(backupCount) %(appPath)' + ], backupCallParams ], + [me.cmd, [ + 'bash /root/%(envName)_backup-logic.sh rotate_snapshots %(baseUrl) %(backupType) %(backupPath) %(backupLogFile) %(envName) %(backupCount) %(appPath) %(session) %(email)' + ], backupCallParams ], + [me.cmd, [ + 'bash /root/%(envName)_backup-logic.sh check_backup_repo %(baseUrl) %(backupType) %(backupPath) %(backupLogFile) %(envName) %(backupCount) %(appPath) %(session) %(email)' + ], backupCallParams ] + ]); + }; + + me.restore = function () { + return me.exec([ + [me.checkEnvStatus], + [me.checkCurrentlyRunningBackup], + [me.addMountForRestore], + [me.cmd, ['echo $(date) %(envName) Restoring the snapshot $(cat /root/.backupid)', 'restic self-update 2>&1', 'if [ -e /root/.backupedenv ]; then REPO_DIR=$(cat /root/.backupedenv); else REPO_DIR="%(envName)"; fi', 'jem service stop', 'SNAPSHOT_ID=$(RESTIC_PASSWORD=$REPO_DIR restic -r /opt/backup/$REPO_DIR snapshots|grep $(cat /root/.backupid)|awk \'{print $1}\')', '[ -n "${SNAPSHOT_ID}" ] || false', 'RESTIC_PASSWORD=$REPO_DIR GOGC=20 restic -r /opt/backup/$REPO_DIR restore ${SNAPSHOT_ID} --target /'], + { + nodeGroup: "cp", + envName: config.envName + }], + [me.cmd, [ + 'echo $(date) %(envName) Restoring the database from snapshot $(cat /root/.backupid)', + '! which mysqld || service mysql start 2>&1', + 'for i in DB_HOST DB_USER DB_PASSWORD DB_NAME; do declare "${i}"=$(cat %(appPath)/wp-config.php | grep ${i} |grep -v \'^[[:space:]]*#\' | tr -d \'[[:blank:]]\' | awk -F \',\' \'{print $2}\' | tr -d "\\"\');"|tr -d \'\\r\'|tail -n 1); done', + 'source /etc/jelastic/metainf.conf ; if [ "${COMPUTE_TYPE}" == "lemp" -o "${COMPUTE_TYPE}" == "llsmp" ]; then wget -O /root/addAppDbUser.sh %(baseUrl)/scripts/addAppDbUser.sh; chmod +x /root/addAppDbUser.sh; bash /root/addAppDbUser.sh ${DB_USER} ${DB_PASSWORD} ${DB_HOST}; fi', + 'mysql -u${DB_USER} -p${DB_PASSWORD} -h ${DB_HOST} --execute="CREATE DATABASE IF NOT EXISTS ${DB_NAME};"', 'mysql -h ${DB_HOST} -u ${DB_USER} -p${DB_PASSWORD} ${DB_NAME} --force < /root/wp_db_backup.sql' + ], + { + envName: config.envName, + baseUrl: config.baseUrl, + appPath: "/var/www/webroot/ROOT" + }], + [me.cmd, ['rm -f /root/.backupid /root/wp_db_backup.sql', 'jem service start'], + { + nodeGroup: "cp", + envName: config.envName + }] + ]); + } + + me.checkEnvStatus = function checkEnvStatus() { + if (!nodeManager.isEnvRunning()) { + return { + result: EnvironmentResponse.ENVIRONMENT_NOT_RUNNING, + error: _("env [%(name)] not running", { + name: config.envName + }) + }; + } + + return { + result: 0 + }; + }; + + me.createScript = function createScript() { + var url = me.getScriptUrl("backup-main.js"), + scriptName = config.scriptName, + scriptBody, resp; + + try { + scriptBody = new Transport().get(url); + + scriptBody = me.replaceText(scriptBody, config); + + //delete the script if it already exists + api.dev.scripting.DeleteScript(scriptName); + + //create a new script + resp = api.dev.scripting.CreateScript(scriptName, "js", scriptBody); + + java.lang.Thread.sleep(1000); + + //build script to avoid caching + api.dev.scripting.Build(scriptName); + } catch (ex) { + resp = { + result: Response.ERROR_UNKNOWN, + error: toJSON(ex) + }; + } + + return resp; + }; + + + me.scheduleBackup = function scheduleBackup() { + var quartz = CronToQuartzConverter.convert(config.cronTime); + + for (var i = quartz.length; i--;) { + var resp = api.utils.scheduler.CreateEnvTask({ + appid: appid, + envName: config.envName, + session: session, + script: config.scriptName, + trigger: "cron:" + quartz[i], + params: { + task: 1, + action: "backup" + } + }); + + if (resp.result !== 0) return resp; + } + + return { + result: 0 + }; + }; + + me.clearScheduledBackups = function clearScheduledBackups() { + var envAppid = config.envAppid, + resp = api.utils.scheduler.GetTasks(envAppid, session); + + if (resp.result != 0) return resp; + + var tasks = resp.objects; + + for (var i = tasks.length; i--;) { + if (tasks[i].script == config.scriptName) { + resp = api.utils.scheduler.RemoveTask(envAppid, session, tasks[i].id); + + if (resp.result != 0) return resp; + } + } + + return resp; + }; + + me.getFileUrl = function (filePath) { + return config.baseUrl + "/" + filePath + "?_r=" + Math.random(); + }; + + me.getScriptUrl = function (scriptName) { + return me.getFileUrl("scripts/" + scriptName); + }; + + me.cmd = function cmd(commands, values, sep) { + return nodeManager.cmd(commands, values, sep, true); + }; + + me.replaceText = function (text, values) { + return new StrSubstitutor(values, "${", "}").replace(text); + }; + + me.exec = function (methods, oScope, bBreakOnError) { + var scope, resp, fn; + + if (!methods.push) { + methods = [Array.prototype.slice.call(arguments)]; + onFail = null; + bBreakOnError = true; + } + + for (var i = 0, n = methods.length; i < n; i++) { + if (!methods[i].push) { + methods[i] = [methods[i]]; + } + + fn = methods[i][0]; + methods[i].shift(); + + log(fn.name + (methods[i].length > 0 ? ": " + methods[i] : "")); + scope = oScope || (methods[methods.length - 1] || {}).scope || this; + resp = fn.apply(scope, methods[i]); + + log(fn.name + ".response: " + resp); + + if (resp.result != 0) { + resp.method = fn.name; + resp.type = "error"; + + if (resp.error) { + resp.message = resp.error; + } + + if (bBreakOnError !== false) break; + } + } + + return resp; + }; + + var CronToQuartzConverter = use("https://raw.githubusercontent.com/jelastic-jps/common/main/CronToQuartzConverter"); + + function use(script) { + var Transport = com.hivext.api.core.utils.Transport, + body = new Transport().get(script + "?_r=" + Math.random()); + + return new(new Function("return " + body)())(session); + } + + function NodeManager(envName, baseDir, logPath) { + var ENV_STATUS_TYPE_RUNNING = 1, + me = this, + envInfo; + + me.isEnvRunning = function () { + var resp = me.getEnvInfo(); + + if (resp.result != 0) { + throw new Error("can't get environment info: " + toJSON(resp)); + } + + return resp.env.status == ENV_STATUS_TYPE_RUNNING; + }; + + me.getEnvInfo = function () { + var resp; + + if (!envInfo) { + resp = api.env.control.GetEnvInfo(envName, session); + if (resp.result != 0) return resp; + + envInfo = resp; + } + + return envInfo; + }; + + me.cmd = function (cmd, values, sep, disableLogging) { + var resp, command; + + values = values || {}; + values.log = values.log || logPath; + cmd = cmd.join ? cmd.join(sep || " && ") : cmd; + + command = _(cmd, values); + + if (!disableLogging) { + log("cmd: " + command); + } + + // Since we're removing specific node handling, we'll execute commands at the environment level + resp = api.env.control.ExecCmdByGroup(envName, session, "cp", toJSON([{ command: command }]), true, false, "root"); + + if (resp.result != 0) { + var title = "Backup failed for " + envName, + text = "Backup failed for the environment " + envName + " of " + user.email + " with error message " + resp.responses[0].errOut; + try { + api.message.email.Send(appid, signature, null, user.email, user.email, title, text); + } catch (ex) { + emailResp = error(Response.ERROR_UNKNOWN, toJSON(ex)); + } + } + return resp; + }; + } + + function log(message) { + Logger.debug(message); + return api.marketplace.console.WriteLog(appid, session, message); + } + + function _(str, values) { + return new StrSubstitutor(values || {}, "%(", ")").replace(str); + } + + me.backup_wp_core = function () { + var backupCallParams = { + envName: config.envName, + backupPath: "/mnt/backup", // Direct path to mounted backup storage + baseUrl: config.baseUrl, + backupType: "wp_core", + backupLogFile: "/var/log/backup_addon.log", + session: session, + email: user.email + }; + + return me.exec([ + [me.checkEnvStatus], + [me.checkCurrentlyRunningBackup], + [me.cmd, [ + 'bash /root/%(envName)_backup-logic.sh backup_wp_core %(baseUrl) %(backupType) %(backupPath) %(backupLogFile) %(envName) %(session) %(email)' + ], backupCallParams] + ]); + }; + me.backup_uploads = function () { + var backupCallParams = { + envName: config.envName, + backupPath: "/mnt/backup", // Direct path to mounted backup storage + baseUrl: config.baseUrl, + backupType: "wp_core", + backupLogFile: "/var/log/backup_addon.log", + session: session, + email: user.email + }; + + return me.exec([ + [me.checkEnvStatus], + [me.checkCurrentlyRunningBackup], + [me.cmd, [ + 'bash /root/%(envName)_backup-logic.sh backup_uploads %(baseUrl) %(backupType) %(backupPath) %(backupLogFile) %(envName) %(session) %(email)' + ], backupCallParams] + ]); + }; + me.backup_database = function () { + var backupCallParams = { + envName: config.envName, + backupPath: "/mnt/backup", // Direct path to mounted backup storage + baseUrl: config.baseUrl, + backupType: "wp_core", + backupLogFile: "/var/log/backup_addon.log", + session: session, + email: user.email + }; + + return me.exec([ + [me.checkEnvStatus], + [me.checkCurrentlyRunningBackup], + [me.cmd, [ + 'bash /root/%(envName)_backup-logic.sh backup_database %(baseUrl) %(backupType) %(backupPath) %(backupLogFile) %(envName) %(session) %(email)' + ], backupCallParams] + ]); + }; + + +} \ No newline at end of file