nová verze
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
readonly EX_OK=0
|
||||
readonly EX_GENERAL=1
|
||||
readonly EX_USAGE=2
|
||||
readonly EX_CONFIG=3
|
||||
readonly EX_DEPENDENCY=4
|
||||
readonly EX_PERMISSION=5
|
||||
readonly EX_NOT_FOUND=6
|
||||
readonly EX_COMMAND_FAILED=10
|
||||
readonly EX_K8S=20
|
||||
readonly EX_DB=30
|
||||
readonly EX_BACKUP=40
|
||||
|
||||
# ERR_MSG=""
|
||||
# ERR_CODE=0
|
||||
|
||||
# err_set() {
|
||||
# ERR_CODE="$1"
|
||||
# ERR_MSG="$2"
|
||||
# return "$ERR_CODE"
|
||||
# }
|
||||
|
||||
# err_clear() {
|
||||
# ERR_CODE=0
|
||||
# ERR_MSG=""
|
||||
# }
|
||||
|
||||
# m_a_riadbStatus() {
|
||||
# err_clear
|
||||
|
||||
# local out err
|
||||
# if ! run out appError kubectl get pods -n mariadb; then
|
||||
# err_set 30 "Cannot get MariaDB pods: $err"
|
||||
# return $ERR_CODE
|
||||
# fi
|
||||
|
||||
# log_table "$out"
|
||||
# }
|
||||
|
||||
|
||||
# Print an error message to stderr.
|
||||
# $1 (message): error message text.
|
||||
function appError() {
|
||||
printf '%s\n' "$*" >&2
|
||||
}
|
||||
|
||||
# Print a fatal error message and exit the script.
|
||||
# [$1] (code): optional numeric exit code (defaults to 1); if omitted, $1 is the message.
|
||||
# $1 (message): error message text (all remaining args when code is provided).
|
||||
function appFailure() {
|
||||
local code=1
|
||||
|
||||
if [[ "${1:-}" =~ ^[0-9]+$ ]]; then
|
||||
code="$1"
|
||||
shift || true
|
||||
fi
|
||||
|
||||
printf '%b\n' "\033[0;91mCrash: $*\033[0m"
|
||||
exit "$code"
|
||||
}
|
||||
|
||||
# Source a shell file, exiting with failure if the file does not exist.
|
||||
# $1 (file): path to the shell file to load.
|
||||
function appLoadFile() {
|
||||
local file="$1"
|
||||
[[ -f "$file" ]] || appFailure 3 "File not found: $file"
|
||||
. "$file"
|
||||
}
|
||||
|
||||
# Validate all required configuration variables and abort on any missing or malformed value.
|
||||
function appCheckConfig() {
|
||||
local value
|
||||
|
||||
local -a values=('hostName' 'hostIp' 'hostUuid' 'hostSalt' 'k3sNamespace' 'redisKube' 'mariadbKube' 'olsKube' 'postfixKube' 'workerKube' 'redisFileUsersAcl' 'postfixIp' 'postfixHost' 'postfixPostmaster' 'postfixDkimSelector' 'workerApiUrl' 'workerName' 'backupType' 'backupFirstDir')
|
||||
for value in "${values[@]}"; do
|
||||
[[ -n "${!value-}" ]] || appFailure 4 "$value: not specified"
|
||||
done
|
||||
|
||||
values=('appAssetsPath' 'appDataPath' 'basePath' 'olsVhostsPath' 'domainsList' 'k3sCmd' 'redisPath' 'mariadbMasterPath' 'mariadbSlavePath' 'olsPath' 'olsAdminPath' 'olsPhpIniPath' 'olsLogsPath' 'olsConfigFile' 'olsPrivatePath' 'olsPublicPath' 'olsVhostsConfigPath' 'postfixPath' 'postfixDkimPath' 'workerPath' 'workerAgentPath' 'workerTasksPath' 'workerFilesPath' 'logPath' 'logFile' 'backupDailyPath' 'backupArchivePath' 'storagePath' 'rsyncPath' 'ftpPath' 'sshPath')
|
||||
for value in "${values[@]}"; do
|
||||
[[ "${!value-}" == /* ]] || appFailure 4 "$value: a variable must begin with /"
|
||||
[[ "${!value-}" != */ ]] || appFailure 4 "$value: a variable cannot end in /"
|
||||
done
|
||||
|
||||
values=('backupParallelJobs' 'k3sReadyRetries' 'k3sReadySleep' 'workerApiGettingPause' 'workerApiSendingPause' 'backupDailyKeep' 'backupArchiveInterval' 'storageDailyKeep' 'storageArchiveKeep' 'olsQuotaBlockLimit' 'olsQuotaInodeLimit')
|
||||
for value in "${values[@]}"; do
|
||||
[[ "${!value-}" =~ ^[0-9]+$ ]] && (( ${!value} >= 1 )) || appFailure 4 "$value: incorrect number value"
|
||||
done
|
||||
|
||||
if [[ "$backupDailySuffix" == "$backupArchiveSuffix" ]]; then
|
||||
appFailure 4 "backupDailySuffix = backupArchiveSuffix"
|
||||
fi
|
||||
}
|
||||
|
||||
# Initialize the application by loading core libraries, config, modules, and commands.
|
||||
function appBootstrap() {
|
||||
local file
|
||||
|
||||
[[ "$EUID" -eq 0 ]] || appFailure 2 "You must run this script as root"
|
||||
|
||||
# Core
|
||||
for file in \
|
||||
"$appPath/libs/main.sh" \
|
||||
"$appPath/libs/print.sh" \
|
||||
"$appPath/libs/file.sh"
|
||||
do
|
||||
appLoadFile "$file"
|
||||
done
|
||||
|
||||
# Config
|
||||
appLoadFile "$appPath/config.sh"
|
||||
appCheckConfig
|
||||
|
||||
# Modules
|
||||
for file in "$appPath/libs/modules/"*.sh; do
|
||||
appLoadFile "$file"
|
||||
done
|
||||
|
||||
# Commands
|
||||
for file in "$appPath/libs/commands/"*.sh; do
|
||||
appLoadFile "$file"
|
||||
done
|
||||
}
|
||||
|
||||
# Dispatch execution to the appropriate router based on the APP_MODE environment variable.
|
||||
function appRouter() {
|
||||
local mode="${APP_MODE:-cmd}"
|
||||
|
||||
case "$mode" in
|
||||
api) apiRouter "$@" ;;
|
||||
cmd|*) cmdRouter "$@" ;;
|
||||
esac
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
backupLabel="[Backup]"
|
||||
|
||||
# Creates archive-based backups for all OpenLiteSpeed virtual hosts.
|
||||
# [$1] (path): destination directory.
|
||||
# [$2] (type): archive type: zip or tar.
|
||||
function cmdBackupSitesArchive() {
|
||||
local path
|
||||
path=$(backupPathGenerate "$1")
|
||||
|
||||
local type="${2:-$backupType}"
|
||||
|
||||
local error vhostList total
|
||||
run vhostList error openlitespeedConfigVhostList || { printDanger "Failed get list of vhosts: $error"; return 1; }
|
||||
total=$(grep -c . <<< "$vhostList")
|
||||
|
||||
printSection "Config"
|
||||
printDotText "Target" "all ($total)"
|
||||
printDotText "Type" "$type"
|
||||
printDotText "Path" "$path"
|
||||
|
||||
printSection "Executing"
|
||||
local domain label num i=0 lockFile tmpDir
|
||||
maxJobs="${backupParallelJobs:-1}"
|
||||
tmpDir=$(mktemp -d) || { printDanger "Failed to create temp directory"; return 1; }
|
||||
lockFile="$tmpDir/.lock"
|
||||
while read -r domain; do
|
||||
((i++))
|
||||
num=$(printf "%0${#total}d" "$i")
|
||||
label="$num: $fontBlue$domain$fontReset"
|
||||
|
||||
while (( $(jobs -rp | wc -l) >= maxJobs )); do
|
||||
wait -n 2>/dev/null || true
|
||||
done
|
||||
|
||||
(
|
||||
local jobError
|
||||
if runError jobError backupSite "$domain" "$path" "$type"; then
|
||||
{ flock 9; printDotText "$label" "$labelDone"; } 9>>"$lockFile"
|
||||
else
|
||||
touch "$tmpDir/$i"
|
||||
{ flock 9; printDotText "$label" "$labelFail"; printDanger "$jobError"; } 9>>"$lockFile"
|
||||
fi
|
||||
) &
|
||||
done < <(awk 'NF' <<< "$vhostList")
|
||||
wait
|
||||
|
||||
local failed=0 f
|
||||
for f in "$tmpDir"/[0-9]*; do
|
||||
[[ -f "$f" ]] || break
|
||||
((failed++))
|
||||
done
|
||||
rm -rf "$tmpDir"
|
||||
|
||||
printSection "Summary"
|
||||
printDotText "Total" "$fontBlue$total$fontReset"
|
||||
printDotText "Successful" "$fontGreen$((total-failed))$fontReset"
|
||||
printDotText "Failed" "$fontRed$failed$fontReset"
|
||||
}
|
||||
|
||||
# Creates rsync-based backups for all sites; first daily backup is full, later ones use hard links.
|
||||
# [$1] (path): destination directory.
|
||||
function cmdBackupSitesRsync() {
|
||||
local path
|
||||
path=$(backupPathGenerate "$1")
|
||||
|
||||
printSection "Config"
|
||||
printDotText "Target" "all"
|
||||
printDotText "Path" "$path"
|
||||
|
||||
local error vhostList
|
||||
run vhostList error openlitespeedConfigVhostList || { printDanger "Failed get list of vhosts: $error"; return 1; }
|
||||
|
||||
printText "Files..."
|
||||
if [[ "$path" == *"$backupFirstDir" ]]; then
|
||||
runError error rsync -a --mkpath -- "$olsVhostsPath/" "$path" || printDanger "$error"
|
||||
else
|
||||
runError error rsync -a --link-dest="$backupDailyPath/$backupFirstDir" -- "$olsVhostsPath/" "$path" || printDanger "$error"
|
||||
fi
|
||||
|
||||
printText "Databases..."
|
||||
while read -r domain; do
|
||||
runError error backupSiteDatabase "$domain" "$path/$domain.sql.tar.gz" || printDanger "$error"
|
||||
done < <(awk 'NF' <<< "$vhostList")
|
||||
|
||||
printText "Configs..."
|
||||
while read -r domain; do
|
||||
runError error cp -p -- "$olsVhostsConfigPath/$domain.conf" "$path/$domain.conf" || printDanger "$error"
|
||||
done < <(awk 'NF' <<< "$vhostList")
|
||||
}
|
||||
|
||||
# Runs a backup for all sites using the configured or given backup type.
|
||||
# [$1] (path): destination directory.
|
||||
# [$2] (type): backup type: zip, tar, or rsync.
|
||||
function cmdBackupSites() {
|
||||
local path="$1"
|
||||
path=$(backupPathGenerate "$path")
|
||||
|
||||
local type="${2:-$backupType}"
|
||||
|
||||
case "$type" in
|
||||
zip|tar)
|
||||
cmdBackupSitesArchive "$path" "$type" || return 1
|
||||
;;
|
||||
rsync)
|
||||
cmdBackupSitesRsync "$path" || return 1
|
||||
;;
|
||||
*)
|
||||
printSection "Config"
|
||||
printDanger "Unknown backup type: $type"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
function cmdBackupArchiveDispatch() {
|
||||
printSection "Config"
|
||||
[[ -n "$backupArchivePath" ]] || { printDotText "Path" "$labelNull"; return 1; }
|
||||
printDotText "Path" "$backupArchivePath"
|
||||
[[ -n "$backupArchiveInterval" ]] || { printDotText "Period" "$labelNull"; return 1; }
|
||||
printDotText "Period" "$backupArchiveInterval"
|
||||
[[ -n "$backupArchiveDirPattern" ]] || { printDotText "Pattern" "$labelNull"; return 1; }
|
||||
printDotText "Pattern" "$backupArchiveDirPattern"
|
||||
|
||||
mkdir -p -- "$backupArchivePath" || { printDanger "Failed to create archive path"; return 1; }
|
||||
|
||||
local dirList archiveList dailyList error
|
||||
run dirList error fileList "$backupArchivePath" d || { printDanger "$error"; return 1; }
|
||||
archiveList=$(printf '%s\n' "$dirList" | grep -E -- "$backupArchiveDirPattern" | sort || true)
|
||||
|
||||
run dirList error fileList "$backupDailyPath" d || { printDanger "$error"; return 1; }
|
||||
dailyList=$(printf '%s\n' "$dirList" | grep -E -- "$backupDailyDirPattern" | sort || true)
|
||||
|
||||
printSection "Directories found"
|
||||
local archiveCount archiveRemoveCount i=0 num label dirDate dirDays archiveRemoveList=""
|
||||
archiveCount=$(grep -c . <<< "$archiveList" || true)
|
||||
if [[ "$archiveCount" -gt 0 ]]; then
|
||||
while read -r dir; do
|
||||
((++i))
|
||||
num=$(printf "%0${#archiveCount}d" "$i")
|
||||
label="$num: $fontBlue$dir$fontReset"
|
||||
|
||||
if (( i == archiveCount )); then
|
||||
printDotText "$label" "${fontGreen}save$fontReset"
|
||||
elif (( i == archiveCount - 1 )); then
|
||||
dirDate="${dir%$backupArchiveSuffix}"
|
||||
dirDays=$(( $(timeDiff "$dirDate" "$appDate") / 86400 ))
|
||||
if (( dirDays >= backupArchiveInterval )); then
|
||||
printDotText "$label" "${fontRed}delete$fontReset"
|
||||
archiveRemoveList+=$'\n'"$dir"
|
||||
else
|
||||
printDotText "$label" "${fontGreen}save$fontReset"
|
||||
fi
|
||||
else
|
||||
printDotText "$label" "${fontRed}delete$fontReset"
|
||||
archiveRemoveList+=$'\n'"$dir"
|
||||
fi
|
||||
done < <(awk 'NF' <<< "$archiveList")
|
||||
|
||||
archiveRemoveCount=$(grep -c . <<< "$archiveRemoveList" || true)
|
||||
if [[ "$archiveRemoveCount" -gt 0 ]]; then
|
||||
printSection "Deleting Directories"
|
||||
|
||||
while read -r dir; do
|
||||
label="$backupArchivePath/$dir"
|
||||
if [[ -n "$dir" ]]; then
|
||||
if rm -rf -- "$backupArchivePath/$dir" &>/dev/null; then
|
||||
printDotText "$label" "$labelDone"
|
||||
else
|
||||
printDotText "$label" "$labelFail"
|
||||
fi
|
||||
fi
|
||||
done < <(awk 'NF' <<< "$archiveRemoveList")
|
||||
fi
|
||||
fi
|
||||
|
||||
# Promote oldest daily (excluding today) to archive
|
||||
run dirList error fileList "$backupArchivePath" d || { printDanger "$error"; return 1; }
|
||||
archiveList=$(printf '%s\n' "$dirList" | grep -E -- "$backupArchiveDirPattern" | sort || true)
|
||||
archiveCount=$(grep -c . <<< "$archiveList" || true)
|
||||
if (( archiveCount < 2 )); then
|
||||
local oldestDaily
|
||||
oldestDaily=$(head -1 <<< "$dailyList" || true)
|
||||
[[ -n "$oldestDaily" ]] || return 0
|
||||
[[ "$oldestDaily" != "$appDate" ]] || return 0
|
||||
|
||||
printSection "Promote Directories"
|
||||
mv -- "$backupDailyPath/$oldestDaily" "$backupArchivePath/${oldestDaily}$backupArchiveSuffix" || {
|
||||
printDotText "$backupDailyPath/$oldestDaily" "$labelFail"
|
||||
printDanger "Failed to promote daily to archive: $oldestDaily"
|
||||
return 1
|
||||
}
|
||||
printDotText "$backupDailyPath/$oldestDaily" "$labelDone"
|
||||
fi
|
||||
}
|
||||
|
||||
function cmdBackupDailyDispatch() {
|
||||
printSection "Config"
|
||||
[[ -n "$backupDailyPath" ]] || { printDotText "Path" "$labelNull"; return 1; }
|
||||
printDotText "Path" "$backupDailyPath"
|
||||
[[ -n "$backupDailyKeep" ]] || { printDotText "Keep" "$labelNull"; return 1; }
|
||||
printDotText "Keep" "$backupDailyKeep"
|
||||
[[ -n "$backupDailyDirPattern" ]] || { printDotText "Pattern" "$labelNull"; return 1; }
|
||||
printDotText "Pattern" "$backupDailyDirPattern"
|
||||
|
||||
mkdir -p -- "$backupDailyPath" || { printDanger "Failed to create archive path"; return 1; }
|
||||
|
||||
local dirList dailyList error
|
||||
run dirList error fileList "$backupDailyPath" d || { printDanger "$error"; return 1; }
|
||||
dailyList=$(printf '%s\n' "$dirList" | grep -v "$appDate" | grep -E -- "$backupDailyDirPattern" | sort || true)
|
||||
|
||||
printSection "Directories found"
|
||||
local dailyCount dailyRemoveCount i=0 num label dailyRemoveList=""
|
||||
dailyCount=$(grep -c . <<< "$dailyList" || true)
|
||||
|
||||
if [[ "$dailyCount" -gt 0 ]]; then
|
||||
while read -r dir; do
|
||||
((++i))
|
||||
num=$(printf "%0${#dailyCount}d" "$i")
|
||||
label="$num: $fontBlue$dir$fontReset"
|
||||
|
||||
if [[ "$dir" == "$appDate" ]]; then
|
||||
printDotText "$label" "${fontGray}skipped$fontReset"
|
||||
elif (( dailyCount - backupDailyKeep >= i )); then
|
||||
printDotText "$label" "${fontRed}delete$fontReset"
|
||||
dailyRemoveList+=$'\n'"$dir"
|
||||
else
|
||||
printDotText "$label" "${fontGreen}save$fontReset"
|
||||
fi
|
||||
done < <(awk 'NF' <<< "$dailyList")
|
||||
|
||||
dailyRemoveCount=$(grep -c . <<< "$dailyRemoveList" || true)
|
||||
if [[ "$dailyRemoveCount" -gt 0 ]]; then
|
||||
printSection "Deleting Directories"
|
||||
while read -r dir; do
|
||||
label="$backupDailyPath/$dir"
|
||||
if [[ -n "$dir" ]]; then
|
||||
if rm -rf -- "$backupDailyPath/$dir" &>/dev/null; then
|
||||
printDotText "$label" "$labelDone"
|
||||
else
|
||||
printDotText "$label" "$labelFail"
|
||||
fi
|
||||
fi
|
||||
done < <(awk 'NF' <<< "$dailyRemoveList")
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
function cmdBackupDispatch() {
|
||||
local second=0 interval=0
|
||||
|
||||
printText "$fontMagenta[Step 1 Processing of archive backups on external storage]$fontReset"
|
||||
cmdStorageBackupArchiveDispatch
|
||||
seconds=$(timeDiff "$appDate ${appTime//-/:}" "$(date +%Y-%m-%d) $(date +%H:%M:%S)")
|
||||
printDotText "Complete" "$(date +%Y-%m-%d) $(date +%H:%M:%S) $fontBlue$((seconds-interval))s$fontReset"
|
||||
interval="$seconds"
|
||||
|
||||
printRow
|
||||
printText "$fontMagenta[Step 2 Processing of daily backups on external storage]$fontReset"
|
||||
cmdStorageBackupDailyDispatch
|
||||
seconds=$(timeDiff "$appDate ${appTime//-/:}" "$(date +%Y-%m-%d) $(date +%H:%M:%S)")
|
||||
printDotText "Complete" "$(date +%Y-%m-%d) $(date +%H:%M:%S) $fontBlue$((seconds-interval))s$fontReset"
|
||||
interval="$seconds"
|
||||
|
||||
printRow
|
||||
printText "$fontMagenta[Step 3 Processing of archive backups on host (SKIP...)]$fontReset"
|
||||
# cmdBackupArchiveDispatch
|
||||
# seconds=$(timeDiff "$appDate ${appTime//-/:}" "$(date +%Y-%m-%d) $(date +%H:%M:%S)")
|
||||
# printDotText "Complete" "$(date +%Y-%m-%d) $(date +%H:%M:%S) $fontBlue$((seconds-interval))s$fontReset"
|
||||
# interval="$seconds"
|
||||
|
||||
printRow
|
||||
printText "$fontMagenta[Step 4 Processing of daily backups on host]$fontReset"
|
||||
cmdBackupDailyDispatch
|
||||
seconds=$(timeDiff "$appDate ${appTime//-/:}" "$(date +%Y-%m-%d) $(date +%H:%M:%S)")
|
||||
printDotText "Complete" "$(date +%Y-%m-%d) $(date +%H:%M:%S) $fontBlue$((seconds-interval))s$fontReset"
|
||||
interval="$seconds"
|
||||
|
||||
printRow
|
||||
printText "$fontMagenta[Step 5 Creating a full daily backup]$fontReset"
|
||||
cmdBackupSites
|
||||
seconds=$(timeDiff "$appDate ${appTime//-/:}" "$(date +%Y-%m-%d) $(date +%H:%M:%S)")
|
||||
printDotText "Complete" "$(date +%Y-%m-%d) $(date +%H:%M:%S) $fontBlue$((seconds-interval))s$fontReset"
|
||||
interval="$seconds"
|
||||
|
||||
printRow
|
||||
printText "$fontMagenta[Step 6 Synchronization with external storage (daily backups)]$fontReset"
|
||||
cmdStorageBackupDailySyncLast
|
||||
seconds=$(timeDiff "$appDate ${appTime//-/:}" "$(date +%Y-%m-%d) $(date +%H:%M:%S)")
|
||||
printDotText "Complete" "$(date +%Y-%m-%d) $(date +%H:%M:%S) $fontBlue$((seconds-interval))s$fontReset"
|
||||
interval="$seconds"
|
||||
|
||||
printRow
|
||||
printText "$fontMagenta[Step 7 Synchronization with external storage (archive backups) (SKIP...)]$fontReset"
|
||||
# cmdStorageBackupArchiveSyncLast
|
||||
# seconds=$(timeDiff "$appDate ${appTime//-/:}" "$(date +%Y-%m-%d) $(date +%H:%M:%S)")
|
||||
# printDotText "Complete" "$(date +%Y-%m-%d) $(date +%H:%M:%S) $fontBlue$((seconds-interval))s$fontReset"
|
||||
}
|
||||
|
||||
# Runs a backup for a single site or all sites.
|
||||
# [$1] (target): site domain or "all".
|
||||
# [$2] (path): destination directory.
|
||||
# [$3] (type): backup type.
|
||||
function cmdBackupRun() {
|
||||
local target="$1"
|
||||
[[ -n "$target" ]] || { printRow; printDanger "Target not specified"; return 1; }
|
||||
|
||||
local path="$2"
|
||||
path=$(backupPathGenerate "$path")
|
||||
|
||||
local type="${3:-$backupType}"
|
||||
|
||||
if [[ "$target" == "all" ]]; then
|
||||
cmdBackupSites "$path" "$type" || return 1
|
||||
else
|
||||
printSection "Config"
|
||||
printDotText "Target" "$target"
|
||||
printDotText "Type" "$type"
|
||||
printDotText "Path" "$path"
|
||||
|
||||
printSection "Executing"
|
||||
|
||||
local label error
|
||||
if runError error backupSite "$target" "$path" "$type"; then
|
||||
printDotText "$target" "$labelDone"
|
||||
else
|
||||
printDotText "$target" "$labelFail"
|
||||
printDanger "$error"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
function cmdBackupList() {
|
||||
printRow
|
||||
|
||||
local dirList archiveList dailyList backupList backupCount error
|
||||
run dirList error fileList "$backupArchivePath" d || { printDanger "$error"; return 1; }
|
||||
archiveList=$(printf '%s\n' "$dirList" | grep -E -- "$backupArchiveDirPattern" | sort || true)
|
||||
|
||||
run dirList error fileList "$backupDailyPath" d || { printDanger "$error"; return 1; }
|
||||
dailyList=$(printf '%s\n' "$dirList" | grep -E -- "$backupDailyDirPattern" | sort || true)
|
||||
|
||||
backupList=$(printf '%s\n' "$archiveList" "$dailyList" | awk 'NF' | sort)
|
||||
backupCount=$(grep -c . <<< "$backupList" || true)
|
||||
local i=0 num dir label
|
||||
while read -r dir; do
|
||||
((++i))
|
||||
num=$(printf "%0${#backupCount}d" "$i")
|
||||
label="$num: $fontBlue$dir$fontReset"
|
||||
|
||||
if listContains "$dir" "$archiveList"; then
|
||||
printDotText "$label" "${fontGreen}archive$fontReset"
|
||||
elif listContains "$dir" "$dailyList"; then
|
||||
printDotText "$label" "${fontGreen}daily$fontReset"
|
||||
else
|
||||
printDotText "$label" "$labelUnknown"
|
||||
fi
|
||||
done < <(awk 'NF' <<< "$backupList")
|
||||
}
|
||||
|
||||
function cmdBackupSize() {
|
||||
local dirList file size total=0 archiveList dailyList
|
||||
|
||||
printSection "Archive: $backupArchivePath"
|
||||
run dirList error fileList "$backupArchivePath" d || { printDanger "$error"; return 1; }
|
||||
archiveList=$(printf '%s\n' "$dirList" | grep -E -- "$backupArchiveDirPattern" | sort || true)
|
||||
while IFS= read -r file; do
|
||||
size=$(pathSize "$backupArchivePath/$file" "gb")
|
||||
printDotText "$file" "$size Gb"
|
||||
(( total += size ))
|
||||
done <<< "$archiveList"
|
||||
|
||||
printSection "Daily: $backupDailyPath"
|
||||
run dirList error fileList "$backupDailyPath" d || { printDanger "$error"; return 1; }
|
||||
dailyList=$(printf '%s\n' "$dirList" | grep -E -- "$backupDailyDirPattern" | sort || true)
|
||||
while IFS= read -r file; do
|
||||
size=$(pathSize "$backupDailyPath/$file" "gb")
|
||||
printDotText "$file" "$size Gb"
|
||||
(( total += size ))
|
||||
done <<< "$dailyList"
|
||||
|
||||
printRow
|
||||
printDotText "total" "$total Gb"
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
function cmdHelpPrint() {
|
||||
printf "%b" "\n$fontYellow $1$fontReset\n$2\n"
|
||||
}
|
||||
|
||||
function cmdHelpK3S() {
|
||||
local title="-k|--k3s <action>"
|
||||
local desc="
|
||||
create - create all resources in k3s
|
||||
remove - remove all resources in k3s
|
||||
info - detail about a k3s
|
||||
status - status about a k3s
|
||||
top - top pod
|
||||
res [<resource>] - get resource info (all|pod|event|pv|pvc|deploy|ds|rs|sts|svc|ing|ip|secret|cm|job|cj|ep|no|ns|cs|netpol)"
|
||||
|
||||
cmdHelpPrint "$title" "$desc"
|
||||
}
|
||||
|
||||
function cmdHelpMariaDB() {
|
||||
local title="-m|--mariadb <action>"
|
||||
local desc="
|
||||
install - install MariaDB in k3s
|
||||
update - update MariaDB in k3s
|
||||
uninstall - uninstall MariaDB from k3s
|
||||
info - detail about a MariaDB
|
||||
status - status about a MariaDB
|
||||
replica - replica rebuild
|
||||
database - database list
|
||||
user - user list
|
||||
dump [all|<database>] [<file>] - dump specified database or all databases from Master
|
||||
dump_slave [<file>] - full dump from Slave"
|
||||
|
||||
cmdHelpPrint "$title" "$desc"
|
||||
}
|
||||
|
||||
function cmdHelpRedis() {
|
||||
local title="-r|--redis <action>"
|
||||
local desc="
|
||||
install - install Redis in k3s
|
||||
update - update Redis in k3s
|
||||
uninstall - uninstall Redis from k3s
|
||||
info - detail about a Redis
|
||||
status - status about a Redis
|
||||
user - user list
|
||||
flush - flush current DB
|
||||
pass - update default (root) pass"
|
||||
|
||||
cmdHelpPrint "$title" "$desc"
|
||||
}
|
||||
|
||||
function cmdHelpOpenLiteSpeed() {
|
||||
local title="-o|--ols <action>"
|
||||
local desc="
|
||||
install - install OpenLiteSpeed in k3s
|
||||
update - update OpenLiteSpeed in k3s
|
||||
uninstall - uninstall OpenLiteSpeed from k3s
|
||||
info - detail about a OpenLiteSpeed
|
||||
list <option> - vhost list (all|up|down)
|
||||
up - unpause vhost
|
||||
down - pause vhost
|
||||
alias - set aliases for domain
|
||||
restart - restart OLS (graceful restart)
|
||||
php_kill all|<domain> - kill lsphp for domain or all domains
|
||||
white_list - WebAdmin white list update
|
||||
allow_list - WebAdmin allow list update
|
||||
alias_list all|<domain> - get aliases for domain or all domains
|
||||
rebuild all|<domain> - rebuild owner and permission files
|
||||
config - check config $olsConfigFile"
|
||||
|
||||
cmdHelpPrint "$title" "$desc"
|
||||
}
|
||||
|
||||
function cmdHelpPostfix() {
|
||||
local title="-p|--postfix <action>"
|
||||
local desc="
|
||||
install - install Postfix in k3s
|
||||
update - update Postfix in k3s
|
||||
uninstall - uninstall Postfix from k3s
|
||||
info - detail about a Postfix
|
||||
status - status about a Postfix
|
||||
user - user list (SASL)
|
||||
dkim [<domain>] - autocreate and display DKIM key for DNS record or all domains !!!"
|
||||
|
||||
cmdHelpPrint "$title" "$desc"
|
||||
}
|
||||
|
||||
function cmdHelpWorker() {
|
||||
local title="-w|--worker <action>"
|
||||
local desc="
|
||||
install - install Worker in k3s
|
||||
update - update Worker in k3s
|
||||
uninstall - uninstall Worker from k3s
|
||||
list - task list
|
||||
down - stop receiving new tasks from the API (pause)
|
||||
up - start receiving new tasks from the API (unpause)
|
||||
task <file> [<domain>] - Execute task !!!!!"
|
||||
|
||||
cmdHelpPrint "$title" "$desc"
|
||||
}
|
||||
|
||||
function cmdHelpMetric() {
|
||||
local title="--metric <action>"
|
||||
local desc="
|
||||
install - install Metric in k3s
|
||||
update - update Metric in k3s
|
||||
uninstall - remove Metric from k3s
|
||||
restart - restart !!!!"
|
||||
|
||||
cmdHelpPrint "$title" "$desc"
|
||||
}
|
||||
|
||||
function cmdHelpSite() {
|
||||
local title="-s|--site <action>"
|
||||
local desc="
|
||||
add <domain> [<pathF>] [<pathD>] - add site (pathF: source files | pathD: source database)
|
||||
add_wp|wp <domain> [<pathF>] [<pathD>] - add site on Wordpress
|
||||
remove <domain> [-f|--force] - site full remove (-f|--force - forced mode)
|
||||
copy <domain> <domainNew> - create copy of site
|
||||
rename <domain> <domainNew> - rename of site
|
||||
info <domain> - view site info and settings
|
||||
status <domain> - check and verify site settings
|
||||
rebuild <domain> - rebuild config for domain (in MariaDB, Redis, OLS, ...)
|
||||
rebuild_wp <domain> - rewrite config connection to internal services in bootstrap.php
|
||||
reset_pass all|<domain> - reset ALL passwords for domain or all domains
|
||||
reset_auth all|<domain> - reset ALL authentication settings for domain or all domains
|
||||
dump <domain> [<file>] - dump database of site"
|
||||
|
||||
cmdHelpPrint "$title" "$desc"
|
||||
}
|
||||
|
||||
function cmdHelpWP() {
|
||||
local title="--wp <action>"
|
||||
local desc="
|
||||
user <domain> - user list
|
||||
pass <domain> <user> <pass> - user password set
|
||||
salt all|<domain> - shuffle salts
|
||||
check all|<domain> - check core and plugins
|
||||
exec all|<domain> <command> - command exec"
|
||||
|
||||
cmdHelpPrint "$title" "$desc"
|
||||
}
|
||||
|
||||
function cmdHelpSftp() {
|
||||
local title="--sftp <action>"
|
||||
local desc="
|
||||
enable <domain> - enable sftp access
|
||||
disable <domain> - disable sftp access
|
||||
reset_pass <domain> - reset pass
|
||||
user - list of users with SFTP access (group: $sftpAccessGroup)
|
||||
support - adding support for SFTP access (group: $sftpAccessGroup)"
|
||||
|
||||
cmdHelpPrint "$title" "$desc"
|
||||
}
|
||||
|
||||
function cmdHelpCron() {
|
||||
local title="--cron <action>"
|
||||
local desc="
|
||||
add - add jobs to cron
|
||||
remove - remove jobs from cron
|
||||
list - task list"
|
||||
|
||||
cmdHelpPrint "$title" "$desc"
|
||||
}
|
||||
|
||||
function cmdHelpBackup() {
|
||||
local title="--backup <action>"
|
||||
local desc="
|
||||
run all|<domain> [<path>] [<type>] - backup
|
||||
path: path to save backup, default: autogenerate
|
||||
type: type backup (zip|tar|archive|rsync), default: $backupType
|
||||
list - list backups on local
|
||||
size - list of backup sizes"
|
||||
|
||||
cmdHelpPrint "$title" "$desc"
|
||||
}
|
||||
|
||||
function cmdHelpRestore() {
|
||||
local title="--restore <domain> $fontRed[NO TESTED!]$fornReset"
|
||||
local desc="
|
||||
run <domain> [<option>] - manual site restore
|
||||
<empty> - manual site restore
|
||||
list - display a list of available markers for restore
|
||||
last - automatic site restore from the last backup
|
||||
full - automatic site restore from the last FULL backup
|
||||
auto - automatic site restore from the last FULL backup or the last backup
|
||||
N - automatic site restore from the last backup #N (N: 1+)"
|
||||
|
||||
cmdHelpPrint "$title" "$desc"
|
||||
}
|
||||
|
||||
function cmdHelpStorage() {
|
||||
local title="--storage [option]"
|
||||
local desc="
|
||||
list - list backups on external storage
|
||||
compare - comparison table
|
||||
size - list of backup sizes
|
||||
sync <type> - synchronization with external storage (archive|daily)
|
||||
remove - remove backups on external storage"
|
||||
|
||||
cmdHelpPrint "$title" "$desc"
|
||||
}
|
||||
|
||||
function cmdHelpScript() {
|
||||
local title="--script <action>"
|
||||
local desc="
|
||||
backup - creating a backup all sites and then synchronizing with external storage
|
||||
backup-long-term - creating long-term backups
|
||||
backup-clean - cleanup of old backups on the current host and on external storage
|
||||
mariadb-dump - creating a backup of all databases in MariaDB
|
||||
worker-observer - launching the task observer
|
||||
metric-kube - send kube metrics to API (Grafana)
|
||||
metric-sites - send sites metrics to API
|
||||
diag-report-ai-short - send diag short report to AI
|
||||
diag-report-ai-full - send diag full report to AI"
|
||||
|
||||
cmdHelpPrint "$title" "$desc"
|
||||
}
|
||||
|
||||
function cmdHelpTest() {
|
||||
local title="--test <action> $fontRed[NO TESTED!]$fornReset"
|
||||
local desc="
|
||||
mariadb - test MariaDB replica
|
||||
redis - test Redis cluster
|
||||
site - test create default site
|
||||
wordpress - test create Wordpress site"
|
||||
|
||||
cmdHelpPrint "$title" "$desc"
|
||||
}
|
||||
|
||||
function cmdHelpMalware() {
|
||||
local title="--malware <action>"
|
||||
local desc="
|
||||
check - check malware in all vhosts
|
||||
remove - remove malware in all vhosts"
|
||||
|
||||
cmdHelpPrint "$title" "$desc"
|
||||
}
|
||||
|
||||
function cmdHelpShell() {
|
||||
local title="--shell [cli]"
|
||||
local desc="
|
||||
opening the shell or cli (if any) in the pods"
|
||||
|
||||
cmdHelpPrint "$title" "$desc"
|
||||
}
|
||||
|
||||
function cmdHelpLog() {
|
||||
local title="--log [parameters]"
|
||||
local desc="
|
||||
opening the logs in the pods. Standard kubectl parameters for logs can be added, for example:
|
||||
-f: logs should be streamed
|
||||
--previous: print the logs for the previous instance of the container in a pod if it exists"
|
||||
|
||||
cmdHelpPrint "$title" "$desc"
|
||||
}
|
||||
|
||||
function cmdHelpDescribe() {
|
||||
local title="--describe <option>"
|
||||
local desc="
|
||||
pod - describe pods
|
||||
node - describe node"
|
||||
|
||||
cmdHelpPrint "$title" "$desc"
|
||||
}
|
||||
|
||||
function cmdHelpDelete() {
|
||||
local title="--delete"
|
||||
local desc="
|
||||
delete pods"
|
||||
|
||||
cmdHelpPrint "$title" "$desc"
|
||||
}
|
||||
|
||||
function cmdHelpInstall() {
|
||||
local title="--install"
|
||||
local desc="
|
||||
install full infrastructure (apk, update ipset/iptables, install k3s, apply yaml...)"
|
||||
|
||||
cmdHelpPrint "$title" "$desc"
|
||||
}
|
||||
|
||||
function cmdHelp() {
|
||||
local title="$appName"
|
||||
local desc="
|
||||
Usage: $0 [arguments...]"
|
||||
|
||||
cmdHelpPrint "$title" "$desc"
|
||||
|
||||
printDotFix 20 "$fontBlue -k|--k3s" "Commands for k3s"
|
||||
printDotFix 20 "$fontBlue -m|--mariadb" "Commands for MariaDB"
|
||||
printDotFix 20 "$fontBlue -r|--redis" "Commands for Redis"
|
||||
printDotFix 20 "$fontBlue -o|--ols" "Commands for Openlitespeed"
|
||||
printDotFix 20 "$fontBlue -p|--postfix" "Commands for Postfix"
|
||||
printDotFix 20 "$fontBlue -w|--worker" "Commands for Worker (agent)"
|
||||
printDotFix 20 "$fontBlue -s|--site" "Commands for Sites"
|
||||
printDotFix 20 "$fontBlue --metric" "Commands for Metrics"
|
||||
printDotFix 20 "$fontBlue --wp" "Commands for Wordpress"
|
||||
printDotFix 20 "$fontBlue --sftp" "Commands for SFTP"
|
||||
printDotFix 20 "$fontBlue --cron" "Commands for Cron"
|
||||
printDotFix 20 "$fontBlue --shell" "Shell or cli (if any) in pods"
|
||||
printDotFix 20 "$fontBlue --log" "Log of pods"
|
||||
printDotFix 20 "$fontBlue --describe" "Describe of pods"
|
||||
printDotFix 20 "$fontBlue --delete" "Delete pods"
|
||||
printDotFix 20 "$fontBlue --backup" "Create backup of sites"
|
||||
printDotFix 20 "$fontBlue --restore" "Restore sites from backups"
|
||||
printDotFix 20 "$fontBlue --storage" "Copy backups to storage"
|
||||
printDotFix 20 "$fontBlue --script" "Execute scripts"
|
||||
printDotFix 20 "$fontBlue --install" "Install full infrastructure"
|
||||
printDotFix 20 "$fontBlue --test" "Testing infrastructure"
|
||||
printDotFix 20 "$fontBlue --malware" "Malware search"
|
||||
printDotFix 20 "$fontBlue --help" "This help"
|
||||
printRow
|
||||
|
||||
cmdHelpK3S
|
||||
cmdHelpMariaDB
|
||||
cmdHelpRedis
|
||||
cmdHelpOpenLiteSpeed
|
||||
cmdHelpPostfix
|
||||
cmdHelpWorker
|
||||
cmdHelpSite
|
||||
cmdHelpMetric
|
||||
cmdHelpWP
|
||||
cmdHelpSftp
|
||||
cmdHelpCron
|
||||
cmdHelpShell
|
||||
cmdHelpLog
|
||||
cmdHelpDescribe
|
||||
cmdHelpDelete
|
||||
cmdHelpBackup
|
||||
cmdHelpRestore
|
||||
cmdHelpStorage
|
||||
cmdHelpScript
|
||||
cmdHelpInstall
|
||||
cmdHelpTest
|
||||
cmdHelpMalware
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
k3sLabel="[K3S]"
|
||||
|
||||
# Interactively lists pods and stores the selected pod name in the given variable.
|
||||
# $1 (varname): name of the variable to store the selected pod name.
|
||||
function cmdK3sPodSelect() {
|
||||
local -n __pod="$1"
|
||||
|
||||
printRow
|
||||
|
||||
local podList error total
|
||||
run podList error k3sPodListStatus || { printDanger "k3sPodListStatus: $error"; return 1; }
|
||||
[[ -n "$podList" ]] || { printRow printDanger "Pods not found"; return 1; }
|
||||
total=$(grep -c . <<< "$podList")
|
||||
|
||||
local i=0 line name status color num
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
((i++))
|
||||
num=$(printf "%${#total}d" "$i")
|
||||
name="${line%%|*}"
|
||||
status="${line#*|}"
|
||||
|
||||
color="$fontYellow"
|
||||
[[ "$status" == "Running" ]] && color="$fontGreen"
|
||||
[[ "$status" == "NotReady" ]] && color="$fontRed"
|
||||
[[ "$status" == "Terminating" ]] && color="$fontRed"
|
||||
|
||||
printDotText "$num: $fontBlue$name$fontReset" "$color$status$fontReset"
|
||||
done <<< "$podList"
|
||||
|
||||
printRow
|
||||
local input item selected
|
||||
read -r -p "Specify the pod number: " input
|
||||
|
||||
printRow
|
||||
[[ -z "$input" ]] && input=0
|
||||
[[ "$input" =~ ^[0-9]+$ ]] || { printDanger "Invalid pod number"; return 1; }
|
||||
item=$((10#$input))
|
||||
selected=$(awk 'NF {n++} n == item {print; exit}' item="$item" <<< "$podList")
|
||||
[[ -n "$selected" ]] || { printDanger "Unknown pod number"; return 1; }
|
||||
|
||||
__pod="${selected%%|*}"
|
||||
}
|
||||
|
||||
# Interactively opens a shell or CLI inside a selected pod.
|
||||
# [$1] (cli): pass "cli" to open the native CLI (mariadb/redis-cli) instead of a shell.
|
||||
function cmdShell() {
|
||||
printTitle " Shell $@"
|
||||
|
||||
local cli="$1"
|
||||
local pod
|
||||
cmdK3sPodSelect pod || return 1
|
||||
|
||||
local resource resourceEx
|
||||
resource=$(printf '%s' "$pod" | sed -E 's/-([0-9a-f]{8,}-[a-z0-9]+|[a-z0-9]{5}|[0-9]+)$//')
|
||||
resourceEx=$(printf '%s' "$pod" | sed -E 's/-([-a-z0-9]+)$//')
|
||||
local container=(-c "$resource")
|
||||
local cmd=(bash)
|
||||
local cmdLog=(bash)
|
||||
case "$resourceEx" in
|
||||
"$redisKube"|"$metricKube"|debug)
|
||||
cmd=(sh)
|
||||
cmdLog=(sh)
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "$cli" == "cli" ]]; then
|
||||
case "$resource" in
|
||||
"$mariadbMasterKube"|"$mariadbSlaveKube")
|
||||
cmd=(mariadb -uroot -p"$mariadbRootPass")
|
||||
cmdLog=(mariadb -uroot -p"********")
|
||||
;;
|
||||
"$redisKube")
|
||||
if [[ -z "$redisRootPass" ]]; then
|
||||
cmd=(redis-cli)
|
||||
cmdLog=(redis-cli)
|
||||
else
|
||||
cmd=(redis-cli --no-auth-warning -a "$redisRootPass")
|
||||
cmdLog=(redis-cli --no-auth-warning -a"********")
|
||||
fi
|
||||
;;
|
||||
"$redisSentinelKube")
|
||||
if [[ -z "$redisRootPass" ]]; then
|
||||
cmd=(redis-cli -p 26379)
|
||||
cmdLog=(redis-cli -p 26379)
|
||||
else
|
||||
cmd=(redis-cli --no-auth-warning -p 26379 -a "$redisRootPass")
|
||||
cmdLog=(redis-cli --no-auth-warning -p 26379 -a"********")
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
printText "$fontBlue$k3sCmd kubectl -n $k3sNamespace exec -it pod/$pod ${container[*]} -- ${cmdLog[*]}$fontReset"
|
||||
printRow
|
||||
k3sRun exec -it "pod/$pod" "${container[@]}" -- "${cmd[@]}"
|
||||
}
|
||||
|
||||
# Interactively selects a pod and streams its logs.
|
||||
# [$@] (...): extra arguments passed to kubectl logs (e.g. -f, --tail=100).
|
||||
function cmdLog() {
|
||||
printTitle " Log $@"
|
||||
|
||||
local pod
|
||||
cmdK3sPodSelect pod || return 1
|
||||
|
||||
local resource
|
||||
resource=$(printf '%s' "$pod" | sed -E 's/-([0-9a-f]{8,}-[a-z0-9]+|[a-z0-9]{5}|[0-9]+)$//')
|
||||
local container=(-c "$resource")
|
||||
|
||||
printText "$fontBlue$k3sCmd kubectl -n $k3sNamespace logs pod/$pod ${container[*]} $*$fontReset"
|
||||
printRow
|
||||
k3sRun logs "pod/$pod" "${container[@]}" "$@"
|
||||
}
|
||||
|
||||
# Interactively selects a pod and describes it; also supports describing a node.
|
||||
# [$1] (target): pod (default) or node.
|
||||
function cmdDescribe() {
|
||||
local target="${1:-pod}"
|
||||
shift || true
|
||||
|
||||
printTitle " Describe $@"
|
||||
|
||||
case "$target" in
|
||||
pod)
|
||||
local pod
|
||||
cmdK3sPodSelect pod || return 1
|
||||
|
||||
printText "$fontBlue$k3sCmd kubectl -n $k3sNamespace describe pod/$pod $*$fontReset"
|
||||
printRow
|
||||
k3sRun describe "pod/$pod" "$@"
|
||||
k3sRun describe "pod/$pod" "$@"
|
||||
;;
|
||||
node)
|
||||
k3sRun describe node "$@"
|
||||
;;
|
||||
*)
|
||||
printRow
|
||||
printWarning "Unsupported or empty command: $target"
|
||||
cmdHelpDescribe
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Interactively selects and deletes a pod.
|
||||
function cmdDelete() {
|
||||
printTitle " Delete $@ $fontRed[DANGER]$fontReset"
|
||||
|
||||
local pod
|
||||
cmdK3sPodSelect pod || return 1
|
||||
|
||||
printText "$fontBlue$k3sCmd kubectl -n $k3sNamespace delete pod/$pod $*$fontReset"
|
||||
printRow
|
||||
k3sRun delete "pod/$pod" "$@"
|
||||
}
|
||||
|
||||
# Lists k3s resources; interactively prompts for type if not provided.
|
||||
# [$1] (resource): resource type abbreviation (pod, deploy, svc, etc.).
|
||||
function cmdK3sResourceList() {
|
||||
local resource="$1"
|
||||
if [[ ! "$resource" =~ ^(all|pod|event|pv|pvc|deploy|ds|rs|sts|svc|ing|ip|secret|cm|job|cj|ep|no|ns|cs|netpol)$ ]]; then
|
||||
local resList="
|
||||
all|Get all resources
|
||||
pod|Pod
|
||||
event|Event
|
||||
pv|PersistentVolume
|
||||
pvc|PersistentVolumeClaim
|
||||
deploy|Deployment
|
||||
ds|DaemonSet
|
||||
rs|ReplicaSet
|
||||
sts|StatefulSet
|
||||
svc|Service
|
||||
ing|Ingress
|
||||
ip|IPAddress
|
||||
secret|Secret
|
||||
cm|ConfigMap
|
||||
job|Job
|
||||
cj|CronJob
|
||||
ep|Endpoint
|
||||
no|Node
|
||||
ns|Namespace
|
||||
cs|ComponentStatuses
|
||||
netpol|NetworkPolicies"
|
||||
|
||||
printRow
|
||||
local i=0 line code info num
|
||||
total=$(grep -c . <<< "$resList")
|
||||
while IFS= read -r line; do
|
||||
[[ -n "$line" ]] || continue
|
||||
((i++))
|
||||
num=$(printf "%${#total}d" "$i")
|
||||
code="${line%%|*}"
|
||||
info="${line#*|}"
|
||||
printDotFix 20 "$num: $fontBlue$code$fontReset" "$info"
|
||||
done <<< "$resList"
|
||||
|
||||
printRow
|
||||
local input item selected
|
||||
read -r -p "Specify the type number [0]: " input
|
||||
|
||||
printRow
|
||||
[[ -z "$input" ]] && input=0
|
||||
[[ "$input" =~ ^[0-9]+$ ]] || { printDanger "Invalid type number"; return 1; }
|
||||
item=$((10#$input))
|
||||
selected=$(awk 'NF {n++} n == item {print; exit}' item="$item" <<< "$resList")
|
||||
[[ -n "$selected" ]] || { printDanger "Unknown type number"; return 1; }
|
||||
resource="${selected%%|*}"
|
||||
fi
|
||||
|
||||
run output error k3sRun get "$resource" || { printDanger "$error"; return 1; }
|
||||
printf '%s\n' "$output"
|
||||
}
|
||||
|
||||
# Installs k3s on the server.
|
||||
function cmdK3sInstall() {
|
||||
printInfo "$k3sLabel Install..."
|
||||
curl -sfL https://get.k3s.io | sh - || return 1
|
||||
systemctl enable --now k3s
|
||||
}
|
||||
|
||||
# Uninstalls k3s from the server.
|
||||
function cmdK3sUninstall() {
|
||||
/usr/local/bin/k3s-uninstall.sh
|
||||
}
|
||||
|
||||
# Adds a namespace to Kubernetes if it does not already exist.
|
||||
# $1 (namespace): namespace name.
|
||||
function cmdK3sNamespaceAdd() {
|
||||
local output error
|
||||
run output error "$k3sCmd" kubectl get ns -o jsonpath="{range .items[*]}{.metadata.name}{'\n'}{end}" || { printDanger "$k3sLabel $error"; return 1; }
|
||||
|
||||
if ! grep -qF -- "$k3sNamespace" <<< "$output"; then
|
||||
run output error "$k3sCmd" kubectl create ns "$k3sNamespace" || { printDanger "$k3sLabel $error"; return 1; }
|
||||
fi
|
||||
}
|
||||
|
||||
# Deletes all resources in the current namespace.
|
||||
function cmdK3sResourceClean() {
|
||||
printWarning "$k3sLabel Namespace: $k3sNamespace | Wiping..."
|
||||
|
||||
k3sRun delete all --all --ignore-not-found --wait=false --timeout=30s --request-timeout=60s || true
|
||||
k3sRun delete cm,secret,ingress,networkpolicy,svc,pvc,job,cronjob --all --ignore-not-found --wait=false --timeout=30s --request-timeout=60s || true
|
||||
|
||||
mapfile -t pvs < <(
|
||||
"$k3sCmd" kubectl get pv -o jsonpath='{range .items[?(@.spec.claimRef.namespace=="'"$k3sNamespace"'")]}{.metadata.name}{"\n"}{end}' 2>/dev/null
|
||||
)
|
||||
local pv
|
||||
for pv in "${pvs[@]}"; do
|
||||
[[ -n "$pv" ]] || continue
|
||||
"$k3sCmd" kubectl patch pv "$pv" --type=merge -p '{"metadata":{"finalizers":[]}}' || true
|
||||
"$k3sCmd" kubectl delete pv "$pv" --wait=false --timeout=15s || true
|
||||
done
|
||||
|
||||
printSuccess "$k3sLabel Namespace: $k3sNamespace | Wiped"
|
||||
}
|
||||
|
||||
# Validates, applies a YAML file, then waits for new pods to become ready.
|
||||
# $1 (file): path to the YAML configuration file.
|
||||
function cmdK3sYamlApplyEx() {
|
||||
local file="$1" output error
|
||||
|
||||
printSection "Applying YAML file"
|
||||
printDotText "file" "$file"
|
||||
|
||||
run output error k3sYamlCheck "$file" || {
|
||||
printDotText "applying" "$labelFail"
|
||||
printDanger "Error checking YAML: ${error:-$output}"
|
||||
return 1
|
||||
}
|
||||
printDotText "checking" "$labelDone"
|
||||
|
||||
k3sPodsSnapshot podList || {
|
||||
printDotText "applying" "$labelFail"
|
||||
printDanger "Failed get list of pods"
|
||||
return 1
|
||||
}
|
||||
|
||||
runError error k3sYamlApply "$file" || {
|
||||
printDotText "applying" "$labelFail"
|
||||
printDanger "Error applied YAML: ${error:-$output}"
|
||||
return 1
|
||||
}
|
||||
printDotText "applying" "$labelDone"
|
||||
|
||||
k3sWaitNewPodsReady podList || return 1
|
||||
}
|
||||
|
||||
# Displays pods, services, ingress, and kube-system services info.
|
||||
function cmdK3sInfo() {
|
||||
local output error line
|
||||
|
||||
printSection "k3s"
|
||||
if ! run output error k3sRun version; then
|
||||
printDanger "$error";
|
||||
else
|
||||
while IFS= read -r line; do
|
||||
printDotText "${line%% Version:*}" "${line##*: }"
|
||||
done < <(awk 'NF' <<< "$output")
|
||||
fi
|
||||
|
||||
if ! run output error "$k3sCmd" kubectl get nodes --no-headers; then
|
||||
printDanger "$error"
|
||||
else
|
||||
local name status roles age version
|
||||
while IFS='|' read -r name status roles age version; do
|
||||
printSection "$name"
|
||||
if [[ "$status" == "Ready" ]]; then
|
||||
printDotText "Status" "$fontGreen$status$fontReset"
|
||||
else
|
||||
printDotText "Status" "$fontRed$status$fontReset"
|
||||
fi
|
||||
printDotText "Roles" "$roles"
|
||||
printDotText "Age" "$age"
|
||||
printDotText "Version" "$version"
|
||||
done < <(awk 'NF{print $1 "|" $2 "|" $3 "|" $4 "|" $5}' <<< "$output")
|
||||
fi
|
||||
}
|
||||
|
||||
# Displays pods, services, ingress, kube-system services, and non-running pod bugs.
|
||||
function cmdK3sNodesStatus() {
|
||||
printSection "Pods"
|
||||
if run output error k3sRun get pods -o wide; then
|
||||
printText "$output"
|
||||
else
|
||||
printDanger "$error"
|
||||
fi
|
||||
|
||||
printSection "Services"
|
||||
if run output error k3sRun get svc -o wide; then
|
||||
printText "$output"
|
||||
else
|
||||
printDanger "$error"
|
||||
fi
|
||||
|
||||
printSection "Ingress"
|
||||
if run output error k3sRun get ing; then
|
||||
printText "$output"
|
||||
else
|
||||
printDanger "$error"
|
||||
fi
|
||||
|
||||
printSection "Services (kube-system)"
|
||||
if run output error "$k3sCmd" kubectl -n kube-system get svc; then
|
||||
printText "$output"
|
||||
else
|
||||
printDanger "$error"
|
||||
fi
|
||||
|
||||
printSection "Bugs..."
|
||||
|
||||
local output error
|
||||
if run output error kubectl get pods -A \
|
||||
--field-selector=status.phase!=Running,status.phase!=Pending \
|
||||
-o custom-columns='NS:.metadata.namespace,POD:.metadata.name,PHASE:.status.phase,REASON:.status.reason,NODE:.spec.nodeName'; then
|
||||
printText "$output"
|
||||
else
|
||||
printDanger "$error"
|
||||
fi
|
||||
|
||||
printText "
|
||||
PHASE | REASON
|
||||
------------------
|
||||
Failed | !=0 Process crashed
|
||||
Failed | Error Process ended with an error
|
||||
Failed | OOMKilled Ran out of memory
|
||||
Failed | Evicted Kubelet evicted the pod (disk/memory pressure)
|
||||
|
||||
Completed/Succeeded - Not error if Job/migration/init task"
|
||||
}
|
||||
|
||||
# Displays resource usage (CPU/memory) for all pods in the namespace.
|
||||
function cmdK3sTopPod() {
|
||||
local output error
|
||||
run output error k3sRun top pod || { printDanger "$error"; return 1; }
|
||||
|
||||
printRow
|
||||
printText "$output"
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
function cmdMalwareFilesDelete() {
|
||||
local action="${1:-list}"
|
||||
local quarantinePath="$appDataPath/malware/${appDate}_${appTime}"
|
||||
local allowedMuPluginFiles="
|
||||
index.php
|
||||
00-hosting-loader.php
|
||||
load.php
|
||||
hosting-wp-domain-rename.php
|
||||
burst_rest_api_optimizer.php
|
||||
elementor-safe-mode.php
|
||||
mailoptin-customizer-optimizer.php
|
||||
wgpwpp-cache.php
|
||||
installatron_hide_status_test.php
|
||||
"
|
||||
|
||||
[[ "$action" == "remove" ]] && printTitle "Malware files and blocks (quarantine)" || printTitle "Malware files and blocks (detect)"
|
||||
|
||||
run vhostsList error fileList "$olsVhostsPath" d || { printDanger "$error"; return 1; }
|
||||
while read -r dir; do
|
||||
local found=0 pluginList wwwPath
|
||||
wwwPath="$olsVhostsPath/$dir/www"
|
||||
|
||||
# mu-plugins: malware (static $a=null) → quarantine + blocker dir
|
||||
run itemList error fileList "$wwwPath/wp-content/mu-plugins/" f || continue
|
||||
while read -r item; do
|
||||
local fullPath="$wwwPath/wp-content/mu-plugins/$item"
|
||||
if grep -qF '($i){static $a=null' "$fullPath"; then
|
||||
(( found++ )) || printSection "$dir"
|
||||
if [[ "$action" == "remove" ]]; then
|
||||
local pluginName="${item%.php}"
|
||||
local pluginDir="$wwwPath/wp-content/plugins/$pluginName"
|
||||
# move mu-plugin file to quarantine
|
||||
malwareQuarantineMove "$fullPath" && printDotText "$item" "${fontRed}malware $labelDone" \
|
||||
|| printDotText "$item" "${fontRed}malware $labelFail"
|
||||
# move plugin to quarantine if it exists
|
||||
if [[ -d "$pluginDir" ]]; then
|
||||
malwareQuarantineMove "$pluginDir" && printDotText "/wp-content/plugins/$pluginName" "${fontRed}malware plugin $labelDone" \
|
||||
|| printDotText "/wp-content/plugins/$pluginName" "${fontRed}malware plugin $labelFail"
|
||||
fi
|
||||
# create a blocker directory (instead of a file)
|
||||
# mkdir -p "$wwwPath/wp-content/mu-plugins/$pluginName.php" 2>/dev/null \
|
||||
# && printDotText "$pluginName.php" "${fontYellow}blocker dir $labelDone" \
|
||||
# || printDotText "$pluginName.php" "${fontRed}blocker dir $labelFail"
|
||||
else
|
||||
printDotText "/wp-content/mu-plugins/$item" "${fontRed}malware$fontReset"
|
||||
fi
|
||||
elif ! listContains "$item" "$allowedMuPluginFiles"; then
|
||||
(( found++ )) || printSection "$dir"
|
||||
printDotText "/wp-content/mu-plugins/$item" "$labelUnknown"
|
||||
fi
|
||||
done < <(awk 'NF' <<< "$itemList")
|
||||
|
||||
# wp2shell (Auto-login to admin)
|
||||
pluginList=$(find "$wwwPath/wp-content/plugins" -maxdepth 1 -type d -name 'wp2shell*' 2>/dev/null)
|
||||
if [ -n "$pluginList" ]; then
|
||||
while IFS= read -r item; do
|
||||
(( found++ )) || printSection "$dir"
|
||||
if [[ "$action" == "remove" ]]; then
|
||||
malwareQuarantineMove "$item" && printDotText "${item#"$wwwPath"}" "${fontRed}malware $labelDone" \
|
||||
|| printDotText "${item#"$wwwPath"}" "${fontRed}malware $labelFail"
|
||||
else
|
||||
printDotText "${item#"$wwwPath"}" "${fontRed}malware$fontReset"
|
||||
fi
|
||||
done <<< "$pluginList"
|
||||
fi
|
||||
|
||||
# wp-static-cache (?)
|
||||
pluginList=$(find "$wwwPath/wp-content/plugins" -maxdepth 1 -type d -name 'wp-static-cache*' 2>/dev/null)
|
||||
if [ -n "$pluginList" ]; then
|
||||
while IFS= read -r item; do
|
||||
(( found++ )) || printSection "$dir"
|
||||
if [[ "$action" == "remove" ]]; then
|
||||
malwareQuarantineMove "$item" && printDotText "${item#"$wwwPath"}" "${fontRed}malware $labelDone" \
|
||||
|| printDotText "${item#"$wwwPath"}" "${fontRed}malware $labelFail"
|
||||
else
|
||||
printDotText "${item#"$wwwPath"}" "${fontRed}malware$fontReset"
|
||||
fi
|
||||
done <<< "$pluginList"
|
||||
fi
|
||||
|
||||
# hex-named php/zip в wp-content, wp-content/cache, themes/*, uploads/**/
|
||||
local hexZipList
|
||||
hexZipList=$(
|
||||
find "$wwwPath/wp-content" -maxdepth 1 -type f -regextype posix-extended -regex '.*/\.?[0-9a-f]{8}\.(php|zip)$' 2>/dev/null
|
||||
find "$wwwPath/wp-content/cache" -maxdepth 1 -type f -regextype posix-extended -regex '.*/\.?[0-9a-f]{8}\.(php|zip)$' 2>/dev/null
|
||||
find "$wwwPath/wp-content/themes" -maxdepth 2 -type f -regextype posix-extended -regex '.*/\.?[0-9a-f]{8}\.(php|zip)$' 2>/dev/null
|
||||
find "$wwwPath/wp-content/uploads" -maxdepth 3 -type f -regextype posix-extended -regex '.*/\.?[0-9a-f]{8}\.(php|zip)$' 2>/dev/null
|
||||
)
|
||||
if [ -n "$hexZipList" ]; then
|
||||
while IFS= read -r item; do
|
||||
(( found++ )) || printSection "$dir"
|
||||
if [[ "$action" == "remove" ]]; then
|
||||
malwareQuarantineMove "$item" && printDotText "${item#"$wwwPath"}" "${fontRed}malware $labelDone" \
|
||||
|| printDotText "${item#"$wwwPath"}" "${fontRed}malware $labelFail"
|
||||
else
|
||||
printDotText "${item#"$wwwPath"}" "${fontRed}malware$fontReset"
|
||||
fi
|
||||
done <<< "$hexZipList"
|
||||
fi
|
||||
|
||||
# wp-content/themes/*/functions.php cut out block SC_TH_BEGIN...SC_TH_END
|
||||
local funcList
|
||||
funcList=$(find "$wwwPath/wp-content/themes" -maxdepth 2 -name "functions.php" 2>/dev/null)
|
||||
if [ -n "$funcList" ]; then
|
||||
while IFS= read -r item; do
|
||||
if grep -qF '/* SC_TH_BEGIN:' "$item" 2>/dev/null; then
|
||||
(( found++ )) || printSection "$dir"
|
||||
if [[ "$action" == "remove" ]]; then
|
||||
# keep the original file in quarantine, then cut the block out
|
||||
malwareQuarantineCopy "$item" \
|
||||
&& sed -i '/\/\* SC_TH_BEGIN:/,/SC_TH_END:[^*]*\*\//d' "$item" \
|
||||
&& printDotText "${item#"$wwwPath"}" "${fontRed}SC_TH block $labelDone" \
|
||||
|| printDotText "${item#"$wwwPath"}" "${fontRed}SC_TH block $labelFail"
|
||||
else
|
||||
printDotText "${item#"$wwwPath"}" "${fontRed}SC_TH block$fontReset"
|
||||
fi
|
||||
fi
|
||||
done <<< "$funcList"
|
||||
fi
|
||||
|
||||
# wp-content/advanced-cache.php cut out block SC_ADV_BEGIN...SC_ADV_END
|
||||
local advCacheFile="$wwwPath/wp-content/advanced-cache.php"
|
||||
if grep -qF '/* SC_ADV_BEGIN:' "$advCacheFile" 2>/dev/null; then
|
||||
(( found++ )) || printSection "$dir"
|
||||
if [[ "$action" == "remove" ]]; then
|
||||
# keep the original file in quarantine, then cut the block out
|
||||
malwareQuarantineCopy "$advCacheFile" \
|
||||
&& sed -i '/\/\* SC_ADV_BEGIN:/,/SC_ADV_END:[^*]*\*\//d' "$advCacheFile" \
|
||||
&& printDotText "${advCacheFile#"$wwwPath"}" "${fontRed}SC_ADV block $labelDone" \
|
||||
|| printDotText "${advCacheFile#"$wwwPath"}" "${fontRed}SC_ADV block $labelFail"
|
||||
else
|
||||
printDotText "${advCacheFile#"$wwwPath"}" "${fontRed}SC_ADV block$fontReset"
|
||||
fi
|
||||
fi
|
||||
|
||||
# wp-content/db.php cut out block SC_DB_BEGIN...SC_DB_END
|
||||
local dbFile="$wwwPath/wp-content/db.php"
|
||||
if grep -qF '/* SC_DB_BEGIN:' "$dbFile" 2>/dev/null; then
|
||||
(( found++ )) || printSection "$dir"
|
||||
if [[ "$action" == "remove" ]]; then
|
||||
# keep the original file in quarantine, then cut the block out
|
||||
malwareQuarantineCopy "$dbFile" \
|
||||
&& sed -i '/\/\* SC_DB_BEGIN:/,/SC_DB_END:[^*]*\*\//d' "$dbFile" \
|
||||
&& printDotText "${dbFile#"$wwwPath"}" "${fontRed}SC_DB block $labelDone" \
|
||||
|| printDotText "${dbFile#"$wwwPath"}" "${fontRed}SC_DB block $labelFail"
|
||||
else
|
||||
printDotText "${dbFile#"$wwwPath"}" "${fontRed}SC_DB block$fontReset"
|
||||
fi
|
||||
fi
|
||||
|
||||
# other
|
||||
# for subdir in wp-content/mu-plugins wp-content/plugins; do
|
||||
# pluginList=$(find "$wwwPath/$subdir" -maxdepth 1 -regextype posix-extended -regex '^.*[^0-9a-f][0-9a-f]{6,8}(\.php)?$' 2>/dev/null)
|
||||
# if [ -n "$pluginList" ]; then
|
||||
# while IFS= read -r item; do
|
||||
# (( found++ )) || printSection "$dir"
|
||||
# printDotText "${item#"$wwwPath"}" "${fontYellow}warning$fontReset"
|
||||
# done <<< "$pluginList"
|
||||
# fi
|
||||
# done
|
||||
done < <(awk 'NF' <<< "$vhostsList")
|
||||
}
|
||||
|
||||
function cmdMalwareUserDelete() {
|
||||
local action="${1:-list}"
|
||||
|
||||
[[ "$action" == "remove" ]] && printTitle "Users (deleting)" || printTitle "Users (detect)"
|
||||
|
||||
local output error
|
||||
if ! run output error malwareUserList; then
|
||||
printDanger "$error"
|
||||
return 1
|
||||
fi
|
||||
local total=0
|
||||
if [[ "$action" == "remove" ]]; then
|
||||
# group user_id on db_name+table_name
|
||||
declare -A ids_map
|
||||
declare -A name_map
|
||||
while IFS=$'\t' read -r db_name user_id user_login user_registered table_name; do
|
||||
[[ -z "$db_name" ]] && continue
|
||||
(( total++ ))
|
||||
local key="${db_name}|${table_name}"
|
||||
ids_map[$key]+="${user_id},"
|
||||
name_map[$key]="$db_name | $table_name"
|
||||
done < <(awk 'NF' <<< "$output")
|
||||
# delete with a single query on the table
|
||||
for key in "${!ids_map[@]}"; do
|
||||
local ids="${ids_map[$key]%,}"
|
||||
local db_name="${key%%|*}"
|
||||
local table_name="${key##*|}"
|
||||
local sql="DELETE FROM \`${db_name}\`.\`${table_name}\` WHERE ID IN (${ids});"
|
||||
local qout qerr
|
||||
if run qout qerr mariadbMasterRootQuery "$sql"; then
|
||||
printDotText "${name_map[$key]}" "${ids} $labelDone"
|
||||
else
|
||||
printDotText "${name_map[$key]}" "${ids} $labelFail"
|
||||
printDanger "$qerr"
|
||||
fi
|
||||
done
|
||||
else
|
||||
while IFS=$'\t' read -r db_name user_id user_login user_registered table_name; do
|
||||
[[ -z "$db_name" ]] && continue
|
||||
(( total++ ))
|
||||
printDotText "${db_name} | ${user_login} (${user_id})" "$labelDanger"
|
||||
done < <(awk 'NF' <<< "$output")
|
||||
fi
|
||||
|
||||
printDotText "Total" "$total"
|
||||
}
|
||||
|
||||
function cmdMalwareOptionsDelete() {
|
||||
local action="${1:-list}"
|
||||
|
||||
[[ "$action" == "remove" ]] && printTitle "Options (deleting)" || printTitle "Options (detect)"
|
||||
|
||||
local output error
|
||||
if ! run output error malwareOptionsList; then
|
||||
printDanger "$error"
|
||||
return 1
|
||||
fi
|
||||
local total=0
|
||||
if [[ "$action" == "remove" ]]; then
|
||||
# group option_id by db_name+table_name
|
||||
declare -A ids_map
|
||||
declare -A name_map
|
||||
while IFS=$'\t' read -r db_name table_name option_id option_name; do
|
||||
[[ -z "$db_name" ]] && continue
|
||||
(( total++ ))
|
||||
local key="${db_name}|${table_name}"
|
||||
ids_map[$key]+="${option_id},"
|
||||
name_map[$key]="$db_name | $table_name"
|
||||
done < <(awk 'NF' <<< "$output")
|
||||
# delete with a single query per table
|
||||
for key in "${!ids_map[@]}"; do
|
||||
local ids="${ids_map[$key]%,}"
|
||||
local db_name="${key%%|*}"
|
||||
local table_name="${key##*|}"
|
||||
local sql="DELETE FROM \`${db_name}\`.\`${table_name}\` WHERE option_id IN (${ids});"
|
||||
local qout qerr
|
||||
if run qout qerr mariadbMasterRootQuery "$sql"; then
|
||||
printDotText "${name_map[$key]}" "${ids} $labelDone"
|
||||
else
|
||||
printDotText "${name_map[$key]}" "${ids} $labelFail"
|
||||
printDanger "$qerr"
|
||||
fi
|
||||
done
|
||||
else
|
||||
while IFS=$'\t' read -r db_name table_name option_id option_name; do
|
||||
[[ -z "$db_name" ]] && continue
|
||||
(( total++ ))
|
||||
printDotText "${db_name}" "${option_id}: ${option_name} $labelDanger"
|
||||
done < <(awk 'NF' <<< "$output")
|
||||
fi
|
||||
printDotText "Total" "$total"
|
||||
}
|
||||
|
||||
function cmdMalwareCronDelete() {
|
||||
local action="${1:-list}"
|
||||
|
||||
[[ "$action" == "remove" ]] && printTitle "Cron (deleting)" || printTitle "Cron (detect)"
|
||||
|
||||
local output error
|
||||
if ! run output error malwareCronList; then
|
||||
printDanger "$error"
|
||||
return 1
|
||||
fi
|
||||
local total=0
|
||||
while IFS=$'\t' read -r db_name table_name option_id option_name; do
|
||||
[[ -z "$db_name" ]] && continue
|
||||
(( total++ ))
|
||||
if [[ "$action" == "remove" ]]; then
|
||||
# remove only 'sc_cron_fetch' entry from the serialized cron array | pattern: i:TIMESTAMP;a:N:{s:13:"sc_cron_fetch";...}}}
|
||||
# local sql="UPDATE \`${db_name}\`.\`${table_name}\`
|
||||
# SET option_value = REGEXP_REPLACE(
|
||||
# option_value,
|
||||
# 'i:[0-9]+;a:1:\\\\{s:13:\"sc_cron_fetch\";a:1:\\\\{[^}]+\\\\}\\\\}\\\\}',
|
||||
# ''
|
||||
# )
|
||||
# WHERE option_id = ${option_id};"
|
||||
# local sql="UPDATE \`${db_name}\`.\`${table_name}\`
|
||||
# SET option_value = REGEXP_REPLACE(
|
||||
# option_value,
|
||||
# 'i:[0-9]+;a:1:\\\\{s:13:\"sc_cron_fetch\";a:1:\\\\{s:[0-9]+:\"[^\"]+\";a:[0-9]+:\\\\{[^}]*\\\\}\\\\}\\\\}\\\\}',
|
||||
# ''
|
||||
# )
|
||||
# WHERE option_id = ${option_id};"
|
||||
local sql="UPDATE \`${db_name}\`.\`${table_name}\` SET option_value = REPLACE(option_value, 's:13:\"sc_cron_fetch\"', 's:16:\"sc_cron_fetch_dis\"') WHERE option_id = ${option_id};"
|
||||
local qout qerr
|
||||
if run qout qerr mariadbMasterRootQuery "$sql"; then
|
||||
printDotText "${db_name}" "sc_cron_fetch $labelDone"
|
||||
else
|
||||
printDotText "${db_name}" "sc_cron_fetch $labelFail"
|
||||
printDanger "$qerr"
|
||||
fi
|
||||
else
|
||||
printDotText "${db_name}" "sc_cron_fetch $labelDanger"
|
||||
fi
|
||||
done < <(awk 'NF' <<< "$output")
|
||||
printDotText "Total" "$total"
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
mariadbLabel="[MariaDB]"
|
||||
|
||||
# Prepares Kubernetes secrets for MariaDB.
|
||||
function cmdMariadbPreparation() {
|
||||
printInfo "$mariadbLabel Preparation..."
|
||||
|
||||
local error
|
||||
runError error k3sRun delete secret "$mariadbKube-secret" --ignore-not-found || {
|
||||
printDotText "preparation" "$labelFail"
|
||||
printDanger "Delete old Secret: $error"
|
||||
return 1
|
||||
}
|
||||
|
||||
runError error k3sRun create secret generic "$mariadbKube-secret" --from-literal=root-password="$mariadbRootPass" --from-literal=replication-password="$mariadbRootPass" || {
|
||||
printDotText "preparation" "$labelFail"
|
||||
printDanger "Create new Secret: $error"
|
||||
return 1
|
||||
}
|
||||
|
||||
printDotText "preparation" "$labelDone"
|
||||
}
|
||||
|
||||
# Renders the MariaDB Kubernetes YAML manifest via Helm.
|
||||
function cmdMariadbYamlRender() {
|
||||
local templateFile="templates/$mariadbKube.yaml"
|
||||
|
||||
printSection "Render YAML file | Helm"
|
||||
printDotText "Template" "$appAssetsPath/k3s/$templateFile"
|
||||
[[ -f "$appAssetsPath/k3s/$templateFile" ]] || {
|
||||
printDanger "Template file not found"
|
||||
return 1
|
||||
}
|
||||
|
||||
local profileCpuFile="$appAssetsPath/k3s/profiles/cpu-$kubeCpuProfile.yaml"
|
||||
printDotText "CPU profile" "$profileCpuFile"
|
||||
[[ -f "$profileCpuFile" ]] || {
|
||||
printDanger "CPU profile file not found"
|
||||
return 1
|
||||
}
|
||||
|
||||
local profileMemoryFile="$appAssetsPath/k3s/profiles/memory-$kubeMemoryProfile.yaml"
|
||||
printDotText "Memory profile" "$profileMemoryFile"
|
||||
[[ -f "$profileMemoryFile" ]] || {
|
||||
printDanger "Memory profile file not found"
|
||||
return 1
|
||||
}
|
||||
|
||||
local varsFile
|
||||
varsFile=$(mktemp "/tmp/$mariadbKube.vars.XXXXXX.yaml") || {
|
||||
printDotText "render" "$labelFail"
|
||||
printDanger "Create temp variables file"
|
||||
return 1
|
||||
}
|
||||
printDotText "Variables" "$varsFile"
|
||||
trap 'rm -f -- "$varsFile"' RETURN
|
||||
cat > "$varsFile" <<EOF
|
||||
mariadbHelm: true
|
||||
namespace: $k3sNamespace
|
||||
mariadb: $mariadbKube
|
||||
mariadbMaster: $mariadbMasterKube
|
||||
mariadbSlave: $mariadbSlaveKube
|
||||
mariadbMasterPath: $mariadbMasterPath
|
||||
mariadbSlavePath: $mariadbSlavePath
|
||||
EOF
|
||||
|
||||
printDotText "Result" "$mariadbYaml"
|
||||
mkdir -p -- "$(dirname -- "$mariadbYaml")" || return 1
|
||||
fileBackup "$mariadbYaml"
|
||||
helm template stack "$appAssetsPath/k3s" -f "$varsFile" -f "$profileCpuFile" -f "$profileMemoryFile" --show-only "$templateFile" > "$mariadbYaml" || {
|
||||
printDotText "render" "$labelFail"
|
||||
printDanger "Render failed"
|
||||
return 1
|
||||
}
|
||||
|
||||
printDotText "render" "$labelDone"
|
||||
}
|
||||
|
||||
# Waits until both MariaDB master and slave respond to SQL queries.
|
||||
function cmdMariadbWaitMasterSlave() {
|
||||
local error step attempts=15
|
||||
|
||||
for ((step=1; step<=attempts; step++)); do
|
||||
runError error mariadbMasterRootQuery "SELECT 1;" && break
|
||||
if [[ "$step" -ne "$attempts" ]]; then
|
||||
printWarning "$mariadbLabel Master node | Waiting..."
|
||||
sleep 4
|
||||
else
|
||||
printDanger "$mariadbLabel Master node | Not responding"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
for ((step=1; step<=attempts; step++)); do
|
||||
runError error mariadbSlaveRootQuery "SELECT 1;" && break
|
||||
if [[ "$step" -ne "$attempts" ]]; then
|
||||
printWarning "$mariadbLabel Slave node | Waiting..."
|
||||
sleep 4
|
||||
else
|
||||
printDanger "$mariadbLabel Slave node | Not responding"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Rebuilds MariaDB replication from master to slave.
|
||||
# [$1] (masterPassword): replication password (defaults to $mariadbRootPass).
|
||||
function cmdMariadbReplicaRebuild() {
|
||||
local masterPassword="${1:-$mariadbRootPass}"
|
||||
|
||||
printInfo "$mariadbLabel Replica rebuild..."
|
||||
|
||||
local output error
|
||||
runError error mariadbMasterRootQuery "SELECT 1;" || {
|
||||
printDanger "$mariadbLabel Master node | Not responding: $error"
|
||||
return 1
|
||||
}
|
||||
runError error mariadbSlaveRootQuery "SELECT 1;" || {
|
||||
printDanger "$mariadbLabel Slave node | Not responding: $error"
|
||||
return 1
|
||||
}
|
||||
|
||||
runError error mariadbMasterRootQuery "CREATE USER IF NOT EXISTS 'replication_user'@'%' IDENTIFIED BY '$masterPassword'; GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'replication_user'@'%'; ALTER USER 'replication_user'@'%' IDENTIFIED BY '$masterPassword';" || {
|
||||
printDanger "$mariadbLabel Master node | Recreated replication user: $error"
|
||||
return 1
|
||||
}
|
||||
printInfo "$mariadbLabel Master node | Recreated replication user"
|
||||
|
||||
local dumpFile="$appDataPath/$mariadbMasterKube/${appDate}_${appTime}_replica_rebuild.sql"
|
||||
printInfo "$mariadbLabel Master node | Exporting full dump..."
|
||||
mariadbMasterRootExport all "$dumpFile" || {
|
||||
printDanger "$mariadbLabel Master node | Export failed"
|
||||
return 1
|
||||
}
|
||||
printInfo "$mariadbLabel Master node | Exported: $dumpFile"
|
||||
|
||||
local masterFile masterPos
|
||||
masterFile=$(grep -m1 -oE "MASTER_LOG_FILE='[^']+'" "$dumpFile" | sed "s/MASTER_LOG_FILE='//;s/'//")
|
||||
masterPos=$(grep -m1 -oE "MASTER_LOG_POS=[0-9]+" "$dumpFile" | sed 's/MASTER_LOG_POS=//')
|
||||
[[ -n "$masterFile" && -n "$masterPos" ]] || {
|
||||
printDanger "$mariadbLabel Master node | Cannot parse MASTER_LOG_FILE/MASTER_LOG_POS from dump"
|
||||
return 1
|
||||
}
|
||||
printInfo "$mariadbLabel Master node | Binlog: $masterFile:$masterPos"
|
||||
|
||||
printInfo "$mariadbLabel Slave node | Stopping replication..."
|
||||
runError error mariadbSlaveRootQuery "STOP SLAVE; RESET SLAVE ALL;" || {
|
||||
printDanger "$mariadbLabel Slave node | Stop/reset failed: $error"
|
||||
return 1
|
||||
}
|
||||
|
||||
printInfo "$mariadbLabel Slave node | Importing full dump..."
|
||||
runShell output error k3sRun exec -i pod/"$mariadbSlaveKube"-0 -c "$mariadbSlaveKube" -- mariadb -u "root" -p"$mariadbRootPass" "<" "$dumpFile" ">" /dev/null || {
|
||||
printDanger "$mariadbLabel Slave node | Import failed: ${error:-$output}"
|
||||
return 1
|
||||
}
|
||||
printInfo "$mariadbLabel Slave node | Import completed"
|
||||
|
||||
runError error mariadbSlaveRootQuery "CHANGE MASTER TO MASTER_HOST='${mariadbMasterKube}', MASTER_PORT=3306, MASTER_USER='replication_user', MASTER_PASSWORD='${masterPassword}', MASTER_LOG_FILE='${masterFile}', MASTER_LOG_POS=${masterPos}; START SLAVE;" || {
|
||||
printDanger "$mariadbLabel Slave node | Configure replication failed: $error"
|
||||
return 1
|
||||
}
|
||||
printInfo "$mariadbLabel Slave node | Replication configured"
|
||||
|
||||
run output error mariadbSlaveRootQuery "SHOW SLAVE STATUS\G" showColumn || {
|
||||
printDanger "$mariadbLabel Slave node | Status: $error"
|
||||
return 1
|
||||
}
|
||||
|
||||
local ioRunning sqlRunning secondsBehind lastError
|
||||
ioRunning=$(printf '%s\n' "$output" | awk -F': ' '/^ *Slave_IO_Running:/{print $2}' | tr -d '[:space:]')
|
||||
sqlRunning=$(printf '%s\n' "$output" | awk -F': ' '/^ *Slave_SQL_Running:/{print $2}' | tr -d '[:space:]')
|
||||
secondsBehind=$(printf '%s\n' "$output" | awk -F': ' '/^ *Seconds_Behind_Master:/{print $2}' | tr -d '[:space:]')
|
||||
lastError=$(printf '%s\n' "$output" | awk -F': ' '/^ *Last_Error:/{print substr($0, index($0,$2))}')
|
||||
if [[ "$ioRunning" != "Yes" || "$sqlRunning" != "Yes" ]]; then
|
||||
printWarning "$mariadbLabel Slave node | IO:${ioRunning:-?} | SQL:${sqlRunning:-?}"
|
||||
[[ -n "$lastError" ]] && printWarning "$mariadbLabel Slave node | Last error: $lastError"
|
||||
return 1
|
||||
fi
|
||||
|
||||
printSuccess "$mariadbLabel Replica rebuild completed | Delay ${secondsBehind:-0}s"
|
||||
}
|
||||
|
||||
# Updates MariaDB Kubernetes resources (limits, config, etc.) without re-initialization.
|
||||
function cmdMariadbUpdate() {
|
||||
cmdMariadbYamlRender || return 1
|
||||
cmdK3sYamlApplyEx "$mariadbYaml" || return 1
|
||||
}
|
||||
|
||||
# Installs MariaDB into Kubernetes and initializes replication.
|
||||
function cmdMariadbInstall() {
|
||||
cmdMariadbPreparation || return 1
|
||||
cmdMariadbYamlRender || return 1
|
||||
cmdK3sYamlApplyEx "$mariadbYaml" || return 1
|
||||
cmdMariadbWaitMasterSlave || return 1
|
||||
cmdMariadbReplicaRebuild || return 1
|
||||
}
|
||||
|
||||
# Uninstalls MariaDB Kubernetes resources.
|
||||
function cmdMariadbUninstall() {
|
||||
"$k3sCmd" kubectl delete -f "$mariadbYaml"
|
||||
}
|
||||
|
||||
# Checks MariaDB root password, replication health, database/user count, and total data size.
|
||||
function cmdMariadbInfo() {
|
||||
local password
|
||||
password=$(mariadbRootPassSecretGet)
|
||||
if [[ "$password" != "$mariadbRootPass" ]]; then
|
||||
printRow
|
||||
printDotText "Root password" "$labelFail"
|
||||
printDanger "Use mariadbRootPassSecretSave"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local podList output error pod phase
|
||||
|
||||
# Master
|
||||
if ! run podList error k3sPodListStatus "$mariadbMasterKube"; then
|
||||
printDanger "Get pods (master): $error"
|
||||
elif [[ -z "$podList" ]]; then
|
||||
printDanger "Pods (master) not found"
|
||||
else
|
||||
while IFS='|' read -r pod phase; do
|
||||
printSection "$pod"
|
||||
|
||||
if [[ "$phase" != "Running" ]]; then
|
||||
printDotText "Phase" "$fontRed$phase$fontReset"
|
||||
else
|
||||
printDotText "Phase" "$fontGreen$phase$fontReset"
|
||||
|
||||
if ! run output error mariadbMasterRootQuery "SELECT VERSION();"; then
|
||||
printDotText "MariaDB status" "$fontRed$labelFail$fontReset"
|
||||
printDanger "$error"
|
||||
continue
|
||||
fi
|
||||
printDotText "MariaDB status" "$labelRunning"
|
||||
printDotText "MariaDB version" "$output"
|
||||
|
||||
local masterStatus file position activeConnections
|
||||
if ! run masterStatus error mariadbMasterRootQuery "SHOW MASTER STATUS\G;" "showColumn"; then
|
||||
printDotText "Replica role" "$fontRed$labelUnknown$fontReset"
|
||||
printDanger "$error"
|
||||
continue
|
||||
fi
|
||||
|
||||
file=$(printf '%s\n' "$masterStatus" | awk '/^ *File:/{print $2}')
|
||||
if [[ -z "$file" ]]; then
|
||||
printDotText "Replica role" "$fontRed$labelUnknown$fontReset"
|
||||
printDanger "Params 'File' not found"
|
||||
continue
|
||||
fi
|
||||
|
||||
position=$(printf '%s\n' "$masterStatus" | awk '/^ *Position:/{print $2}')
|
||||
if [[ -z "$position" ]]; then
|
||||
printDotText "Replica role" "$fontRed$labelUnknown$fontReset"
|
||||
printDanger "Params 'Position' not found"
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! run output error mariadbMasterRootQuery "SHOW STATUS LIKE 'Threads_connected';"; then
|
||||
printDotText "Replica role" "$fontRed$labelUnknown$fontReset"
|
||||
printDanger "$error"
|
||||
continue
|
||||
fi
|
||||
|
||||
printDotText "Replica role" "${fontGreen}master$fontReset"
|
||||
|
||||
activeConnections=$(printf '%s\n' "$output" | awk '{print $2}')
|
||||
if [[ "$activeConnections" -ge 100 || "$activeConnections" -eq 1 ]]; then
|
||||
printDotText "Active connections" "$fontYellow$activeConnections$fontReset"
|
||||
else
|
||||
printDotText "Active connections" "$fontGreen$activeConnections$fontReset"
|
||||
fi
|
||||
fi
|
||||
done < <(awk 'NF' <<< "$podList")
|
||||
fi
|
||||
|
||||
# Slave
|
||||
if ! run podList error k3sPodListStatus "$mariadbSlaveKube"; then
|
||||
printDanger "Get pods (slave): $error"
|
||||
elif [[ -z "$podList" ]]; then
|
||||
printDanger "Pods (slave) not found"
|
||||
else
|
||||
while IFS='|' read -r pod phase; do
|
||||
printSection "$pod"
|
||||
|
||||
if [[ "$phase" != "Running" ]]; then
|
||||
printDotText "Phase" "$fontRed$phase$fontReset"
|
||||
else
|
||||
printDotText "Phase" "$fontGreen$phase$fontReset"
|
||||
|
||||
if ! run output error mariadbSlaveRootQuery "SELECT VERSION();"; then
|
||||
printDotText "MariaDB status" "$fontRed$labelFail$fontReset"
|
||||
printDanger "$error"
|
||||
continue
|
||||
fi
|
||||
printDotText "MariaDB status" "$labelRunning"
|
||||
printDotText "MariaDB version" "$output"
|
||||
|
||||
local slaveStatus slaveIoRunning slaveSqlRunning lastError secondsBehindMaster
|
||||
if ! run slaveStatus error mariadbSlaveRootQuery "SHOW SLAVE STATUS\G;" "showColumn"; then
|
||||
printDotText "Replica role" "$fontRed$labelUnknown$fontReset"
|
||||
printDanger "$error"
|
||||
continue
|
||||
fi
|
||||
|
||||
slaveIoRunning=$(printf '%s\n' "$slaveStatus" | awk '/^ *Slave_IO_Running:/{print $2}')
|
||||
if [[ "$slaveIoRunning" != "Yes" ]]; then
|
||||
printDotText "Slave IO Running" "$fontRed$slaveIoRunning$fontReset"
|
||||
continue
|
||||
fi
|
||||
|
||||
slaveSqlRunning=$(printf '%s\n' "$slaveStatus" | awk '/^ *Slave_SQL_Running:/{print $2}')
|
||||
if [[ "$slaveSqlRunning" != "Yes" ]]; then
|
||||
printDotText "Slave SQL Running" "$fontRed$slaveSqlRunning$fontReset"
|
||||
continue
|
||||
fi
|
||||
|
||||
lastError=$(printf '%s\n' "$slaveStatus" | awk '/^ *Last_Error:/{$1=""; sub(/^[ \t]+/,""); print}')
|
||||
if [[ -n "$lastError" ]]; then
|
||||
printDotText "Slave SQL Running" "$fontRed$slaveSqlRunning$fontReset"
|
||||
printDanger "Last error: $lastError"
|
||||
continue
|
||||
fi
|
||||
|
||||
printDotText "Replica role" "${fontGreen}slave$fontReset"
|
||||
|
||||
secondsBehindMaster=$(printf '%s\n' "$slaveStatus" | awk '/^ *Seconds_Behind_Master:/{print $2}')
|
||||
if [[ "$secondsBehindMaster" -ne 0 ]]; then
|
||||
printDotText "Replication lags" "$fontYellow${secondsBehindMaster}s$fontReset"
|
||||
else
|
||||
printDotText "Replication lags" "${fontGreen}0s$fontReset"
|
||||
fi
|
||||
fi
|
||||
done < <(awk 'NF' <<< "$podList")
|
||||
fi
|
||||
|
||||
printSection "MariaDB"
|
||||
|
||||
local databaseList userList dataSize
|
||||
if run output error mariadbDatabaseListGet; then
|
||||
mapfile -t databaseList < <(awk 'NF' <<< "$output")
|
||||
printDotText "Databases" "${#databaseList[@]}"
|
||||
else
|
||||
printDotText "Databases" "$labelUnknown"
|
||||
printDanger "$error"
|
||||
fi
|
||||
|
||||
if run output error mariadbUserListGet; then
|
||||
mapfile -t userList < <(awk 'NF' <<< "$output")
|
||||
printDotText "Users" "${#userList[@]}"
|
||||
else
|
||||
printDotText "Users" "$labelUnknown"
|
||||
printDanger "$error"
|
||||
fi
|
||||
|
||||
if ! run dataSize error mariadbMasterRootQuery "SELECT ROUND(COALESCE(SUM(DATA_LENGTH + INDEX_LENGTH), 0) / 1024 / 1024 / 1024, 2) AS t FROM information_schema.TABLES WHERE TABLE_TYPE = 'BASE TABLE';"; then
|
||||
printDotText "Size" "$labelUnknown"
|
||||
printDanger "$error"
|
||||
else
|
||||
printDotText "Size" "$dataSize Gb"
|
||||
fi
|
||||
}
|
||||
|
||||
# Shows MariaDB master and slave replication status.
|
||||
function cmdMariadbStatus() {
|
||||
local output error
|
||||
|
||||
printSection "$mariadbMasterKube"-0
|
||||
if ! run output error mariadbMasterRootQuery "SHOW MASTER STATUS;"; then
|
||||
printDanger "$error"
|
||||
else
|
||||
printText "$output"
|
||||
fi
|
||||
|
||||
printSection "$mariadbSlaveKube"-0
|
||||
if ! run output error mariadbSlaveRootQuery "SHOW SLAVE STATUS\G;" showColumn; then
|
||||
printDanger "$error"
|
||||
else
|
||||
printText "$output"
|
||||
fi
|
||||
}
|
||||
|
||||
# Lists all MariaDB databases.
|
||||
function cmdMariadbDatabase() {
|
||||
local output error
|
||||
|
||||
printRow
|
||||
|
||||
if ! run output error mariadbDatabaseListGet; then
|
||||
printDanger "$error"
|
||||
elif [[ -z "$output" ]]; then
|
||||
printText "$labelNull"
|
||||
else
|
||||
printText "$output"
|
||||
fi
|
||||
}
|
||||
|
||||
# Lists all MariaDB users.
|
||||
function cmdMariadbUser() {
|
||||
local output error
|
||||
|
||||
printRow
|
||||
|
||||
if ! run output error mariadbUserListGet; then
|
||||
printDanger "$error"
|
||||
elif [[ -z "$output" ]]; then
|
||||
printText "$labelNull"
|
||||
else
|
||||
printText "$output"
|
||||
fi
|
||||
}
|
||||
|
||||
# Exports a full dump from the MariaDB master node.
|
||||
# [$@] (...): arguments passed to mariadbMasterRootExport (e.g. database name, file).
|
||||
function cmdMariadbMasterDump() {
|
||||
local output error
|
||||
|
||||
printRow
|
||||
|
||||
if ! run output error mariadbMasterRootExport "$@"; then
|
||||
printDanger "$error"
|
||||
else
|
||||
printText "$output"
|
||||
fi
|
||||
}
|
||||
|
||||
# Exports a full dump from the MariaDB slave node.
|
||||
# [$@] (...): arguments passed to mariadbSlaveRootExport (e.g. database name, file).
|
||||
function cmdMariadbSlaveDump() {
|
||||
local output error
|
||||
|
||||
printRow
|
||||
|
||||
if ! run output error mariadbSlaveRootExport "$@"; then
|
||||
printDanger "$error"
|
||||
else
|
||||
printText "$output"
|
||||
fi
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
metricLabel="[Metric]"
|
||||
|
||||
# Copies vmagent config file to the configured metric path.
|
||||
function cmdMetricPreparation() {
|
||||
printSection "Preparation..."
|
||||
|
||||
mkdir -p -- "$(dirname -- "$metricVmagentPath")" || return 1
|
||||
cp -f -- "$appAssetsPath/metric/vmagent.yml" "$metricVmagentPath/vmagent.yml"
|
||||
|
||||
printDotText "preparation" "$labelDone"
|
||||
}
|
||||
|
||||
# Renders the Metric Kubernetes YAML manifest via Helm.
|
||||
function cmdMetricYamlRender() {
|
||||
local templateFile="templates/$metricKube.yaml"
|
||||
|
||||
printSection "Render YAML file | Helm"
|
||||
printDotText "Template" "$appAssetsPath/k3s/$templateFile"
|
||||
[[ -f "$appAssetsPath/k3s/$templateFile" ]] || {
|
||||
printDanger "Template file not found"
|
||||
return 1
|
||||
}
|
||||
|
||||
local varsFile
|
||||
varsFile=$(mktemp "/tmp/$metricKube.vars.XXXXXX.yaml") || {
|
||||
printDotText "render" "$labelFail"
|
||||
printDanger "Create temp variables file"
|
||||
return 1
|
||||
}
|
||||
printDotText "Variables" "$varsFile"
|
||||
trap 'rm -f -- "$varsFile"' RETURN
|
||||
cat > "$varsFile" <<EOF
|
||||
metricHelm: true
|
||||
namespace: $k3sNamespace
|
||||
metric: $metricKube
|
||||
metricApiUrl: $metricApiUrl
|
||||
metricPromPath: $metricPromPath
|
||||
metricVmagentPath: $metricVmagentPath
|
||||
EOF
|
||||
|
||||
printDotText "Result" "$metricYaml"
|
||||
mkdir -p -- "$(dirname -- "$metricYaml")" || return 1
|
||||
fileBackup "$metricYaml"
|
||||
helm template stack "$appAssetsPath/k3s" -f "$varsFile" --show-only "$templateFile" > "$metricYaml" || {
|
||||
printDotText "render" "$labelFail"
|
||||
printDanger "Render failed"
|
||||
return 1
|
||||
}
|
||||
|
||||
printDotText "render" "$labelDone"
|
||||
}
|
||||
|
||||
function cmdMetricxUpdate() {
|
||||
cmdMetricYamlRender || return 1
|
||||
cmdK3sYamlApplyEx "$metricYaml" || return 1
|
||||
}
|
||||
|
||||
# Installs Metric components into Kubernetes.
|
||||
function cmdMetricInstall() {
|
||||
cmdMetricPreparation || return 1
|
||||
cmdMetricYamlRender || return 1
|
||||
cmdK3sYamlApplyEx "$metricYaml" || return 1
|
||||
}
|
||||
|
||||
# Uninstalls Metric Kubernetes resources.
|
||||
function cmdMetricUninstall() {
|
||||
"$k3sCmd" kubectl delete -f "$metricYaml"
|
||||
}
|
||||
@@ -0,0 +1,498 @@
|
||||
openlitespeedLabel="[OpenLiteSpeed]"
|
||||
|
||||
# Renders the OpenLiteSpeed Kubernetes YAML manifest via Helm.
|
||||
function cmdOpenlitespeedYamlRender() {
|
||||
local templateFile="templates/$olsKube.yaml"
|
||||
|
||||
printSection "Render YAML file | Helm"
|
||||
printDotText "Template" "$appAssetsPath/k3s/$templateFile"
|
||||
[[ -f "$appAssetsPath/k3s/$templateFile" ]] || {
|
||||
printDanger "Template file not found"
|
||||
return 1
|
||||
}
|
||||
|
||||
local profileCpuFile="$appAssetsPath/k3s/profiles/cpu-$kubeCpuProfile.yaml"
|
||||
printDotText "CPU profile" "$profileCpuFile"
|
||||
[[ -f "$profileCpuFile" ]] || {
|
||||
printDanger "CPU profile file not found"
|
||||
return 1
|
||||
}
|
||||
|
||||
local profileMemoryFile="$appAssetsPath/k3s/profiles/memory-$kubeMemoryProfile.yaml"
|
||||
printDotText "Memory profile" "$profileMemoryFile"
|
||||
[[ -f "$profileMemoryFile" ]] || {
|
||||
printDanger "Memory profile file not found"
|
||||
return 1
|
||||
}
|
||||
|
||||
local adminWhiteList=() adminWhiteStr
|
||||
for i in "${!olsAdminWhiteList[@]}"; do
|
||||
adminWhiteList[$i]="\"${olsAdminWhiteList[$i]}\""
|
||||
done
|
||||
adminWhiteStr=$(IFS=','; printf '%s\n' "${adminWhiteList[*]}")
|
||||
|
||||
local varsFile
|
||||
varsFile=$(mktemp "/tmp/$olsKube.vars.XXXXXX.yaml") || {
|
||||
printDotText "render" "$labelFail"
|
||||
printDanger "Create temp variables file"
|
||||
return 1
|
||||
}
|
||||
printDotText "Variables" "$varsFile"
|
||||
trap 'rm -f -- "$varsFile"' RETURN
|
||||
cat > "$varsFile" <<EOF
|
||||
openlitespeedHelm: true
|
||||
namespace: $k3sNamespace
|
||||
openlitespeed: $olsKube
|
||||
olsPodVhostsPath: $olsPodVhostsPath
|
||||
olsPodPrivatePath: $olsPodPrivatePath
|
||||
olsPodPublicPath: $olsPodPublicPath
|
||||
olsVhostsPath: $olsVhostsPath
|
||||
olsPrivatePath: $olsPrivatePath
|
||||
olsPublicPath: $olsPublicPath
|
||||
olsConfigPath: $olsConfigPath
|
||||
olsPhpIniPath: $olsPhpIniPath
|
||||
olsLogsPath: $olsLogsPath
|
||||
olsAdminPath: $olsAdminPath
|
||||
olsAdminWhiteList: $adminWhiteStr
|
||||
EOF
|
||||
|
||||
printDotText "Result" "$olsYaml"
|
||||
mkdir -p -- "$(dirname -- "$olsYaml")" || return 1
|
||||
fileBackup "$olsYaml"
|
||||
helm template stack "$appAssetsPath/k3s" -f "$varsFile" -f "$profileCpuFile" -f "$profileMemoryFile" --show-only "$templateFile" > "$olsYaml" || {
|
||||
printDotText "render" "$labelFail"
|
||||
printDanger "Render failed"
|
||||
return 1
|
||||
}
|
||||
|
||||
printDotText "render" "$labelDone"
|
||||
}
|
||||
|
||||
# Initializes OpenLiteSpeed after Kubernetes deployment: patches Traefik, sets admin credentials, PHP config, rules, and restarts.
|
||||
function cmdOpenlitespeedInit() {
|
||||
printSection "Initialization..."
|
||||
|
||||
local ug
|
||||
ug="$(stat -c "%u:%g" "$olsConfigFile")"
|
||||
|
||||
# Patch svc traefik
|
||||
runSilent "$k3sCmd" kubectl -n kube-system patch svc traefik -p '{"spec": {"externalTrafficPolicy": "Local"}}' || {
|
||||
printDotText "Patch svc traefik" "$labelFail"
|
||||
return 1
|
||||
}
|
||||
printDotText "Patch svc traefik" "$labelDone"
|
||||
|
||||
cmdOpenlitespeedWaitReady || return 1
|
||||
|
||||
# Updating Administrator Password
|
||||
runSilent cmdOpenlitespeedAdminPassUpdate "$olsAdminPass" || {
|
||||
printDotText "Updating admin password" "$labelFail"
|
||||
return 1
|
||||
}
|
||||
printDotText "Updating admin password" "$labelDone"
|
||||
|
||||
# Adding redirect rules
|
||||
mkdir -p "$olsConfigPath/rules"
|
||||
cp -n "$appAssetsPath"/openlitespeed/rules/* "$olsConfigPath/rules/"
|
||||
chown -R "$ug" "$olsConfigPath/rules"
|
||||
chmod 750 -R "$olsConfigPath/rules"
|
||||
printDotText "Adding redirect rules" "$labelDone"
|
||||
|
||||
# Adding PHP configs
|
||||
local profileFile="$appAssetsPath/openlitespeed/profiles/memory-$kubeMemoryProfile.config"
|
||||
local children max_memory_limit memory_limit max_execution_time post_max_size upload_max_filesize
|
||||
children=$(configGet "$profileFile" "children")
|
||||
max_memory_limit=$(configGet "$profileFile" "max_memory_limit")
|
||||
memory_limit=$(configGet "$profileFile" "memory_limit")
|
||||
max_execution_time=$(configGet "$profileFile" "max_execution_time")
|
||||
post_max_size=$(configGet "$profileFile" "post_max_size")
|
||||
upload_max_filesize=$(configGet "$profileFile" "upload_max_filesize")
|
||||
|
||||
local phpIniFile="$olsPhpIniPath/00-ols-master.ini"
|
||||
fileBackup "$phpIniFile"
|
||||
cp -f -- "$appAssetsPath/openlitespeed/php/00-ols-master.ini" "$phpIniFile"
|
||||
sed -i \
|
||||
-e "s|{{children}}|$children|g" \
|
||||
-e "s|{{max_memory_limit}}|$max_memory_limit|g" \
|
||||
-e "s|{{memory_limit}}|$memory_limit|g" \
|
||||
-e "s|{{max_execution_time}}|$max_execution_time|g" \
|
||||
-e "s|{{post_max_size}}|$post_max_size|g" \
|
||||
-e "s|{{upload_max_filesize}}|$upload_max_filesize|g" \
|
||||
-- "$phpIniFile"
|
||||
find "$olsPhpIniPath" -type f -exec chmod 644 {} +
|
||||
printDotText "Adding PHP configs" "$labelDone"
|
||||
|
||||
# Path OLS Admin config
|
||||
runSilent openlitespeedAdminAllowList || {
|
||||
printDotText "Path OLS Admin config" "$labelFail"
|
||||
return 1
|
||||
}
|
||||
printDotText "Path OLS Admin config" "$labelDone"
|
||||
|
||||
# Path OLS config
|
||||
openlitespeedConfigRebuild || {
|
||||
printDotText "Path OLS config" "$labelFail"
|
||||
return 1
|
||||
}
|
||||
printDotText "Path OLS config" "$labelDone"
|
||||
|
||||
cmdOpenlitespeedRestart
|
||||
cmdOpenlitespeedPhpKill all
|
||||
}
|
||||
|
||||
# Updates OpenLiteSpeed Kubernetes resources (limits, replicas, etc.) without re-initialization.
|
||||
function cmdOpenlitespeedUpdate() {
|
||||
cmdOpenlitespeedYamlRender || return 1
|
||||
cmdK3sYamlApplyEx "$olsYaml" || return 1
|
||||
}
|
||||
|
||||
# Installs OpenLiteSpeed into Kubernetes and runs initialization.
|
||||
function cmdOpenlitespeedInstall() {
|
||||
cmdOpenlitespeedYamlRender || return 1
|
||||
cmdK3sYamlApplyEx "$olsYaml" || return 1
|
||||
cmdOpenlitespeedInit
|
||||
}
|
||||
|
||||
# Uninstalls OpenLiteSpeed Kubernetes resources.
|
||||
function cmdOpenlitespeedUninstall() {
|
||||
"$k3sCmd" kubectl delete -f "$olsYaml"
|
||||
}
|
||||
|
||||
# Waits until OpenLiteSpeed WebAdmin PHP is ready.
|
||||
function cmdOpenlitespeedWaitReady() {
|
||||
local tries=${k3sReadyRetries:-10}
|
||||
local sleepSec=${k3sReadySleep:-2}
|
||||
local i output error
|
||||
for ((i=1; i<=tries; i++)); do
|
||||
run output error openlitespeedExec /usr/local/lsws/admin/fcgi-bin/admin_php -v && return 0
|
||||
printWarning "$openlitespeedLabel Waiting..."
|
||||
sleep "$sleepSec"
|
||||
done
|
||||
|
||||
printDanger "$openlitespeedLabel Not ready after ${tries} tries"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Updates OpenLiteSpeed WebAdmin credentials.
|
||||
# $1 (password): WebAdmin password.
|
||||
# [$2] (user): WebAdmin username (default: admin).
|
||||
function cmdOpenlitespeedAdminPassUpdate() {
|
||||
local password="$1"
|
||||
[[ -n "$password" ]] || { printDanger "$openlitespeedLabel Password not specified"; return 1; }
|
||||
|
||||
local user="${2:-admin}"
|
||||
local encrypt output error
|
||||
|
||||
run encrypt error openlitespeedExec /usr/local/lsws/admin/fcgi-bin/admin_php -q /usr/local/lsws/admin/misc/htpasswd.php "$password" || {
|
||||
printDotText "Get encrypt" "$labelFail"
|
||||
printDanger "$error"
|
||||
return 1
|
||||
}
|
||||
printDotText "Get encrypt" "$labelDone"
|
||||
|
||||
run output error openlitespeedExec bash -c "echo '$user:$encrypt' > /usr/local/lsws/admin/conf/htpasswd" || {
|
||||
printDotText "Save data" "$labelFail"
|
||||
printDanger "$error"
|
||||
return 1
|
||||
}
|
||||
printDotText "Save data" "$labelDone"
|
||||
|
||||
# if admin... save to <hostname>.openlitespeed
|
||||
}
|
||||
|
||||
# Restarts OpenLiteSpeed on all OLS pods.
|
||||
function cmdOpenlitespeedRestart() {
|
||||
local podList error
|
||||
run podList error k3sPodListStatus "$olsKube" || { printDanger "Get pods: $error"; return 1; }
|
||||
[[ -n "$podList" ]] || { printDanger "Pods not found"; return 1; }
|
||||
|
||||
local pod phase output
|
||||
while IFS='|' read -r pod phase; do
|
||||
printSection "$pod"
|
||||
|
||||
if [[ "$phase" != "Running" ]]; then
|
||||
printDotText "Phase" "$fontRed$phase$fontReset"
|
||||
else
|
||||
printDotText "Phase" "$fontGreen$phase$fontReset"
|
||||
|
||||
if ! run output error openlitespeedPodExec "$pod" /usr/local/lsws/bin/lswsctrl restart; then
|
||||
printDotText "Restart" "$labelUnknown"
|
||||
printDanger "$error"
|
||||
elif [[ "$output" =~ "[OK]" ]]; then
|
||||
printDotText "Restart" "$fontGreen$output$fontReset"
|
||||
else
|
||||
printDotText "Restart" "$fontRed$output$fontReset"
|
||||
fi
|
||||
fi
|
||||
done < <(awk 'NF' <<< "$podList")
|
||||
}
|
||||
|
||||
# Restarts PHP workers on all OLS pods.
|
||||
function cmdOpenlitespeedPhpKill() {
|
||||
local target="$1"
|
||||
[[ -n "$target" ]] || { printRow; printDanger "Target not specified"; return 1; }
|
||||
|
||||
local podList error
|
||||
run podList error k3sPodListStatus "$olsKube" || { printRow; printDanger "Get pods: $error"; return 1; }
|
||||
[[ -n "$podList" ]] || { printRow; printDanger "Pods not found"; return 1; }
|
||||
|
||||
local uid=""
|
||||
if [[ "$target" != "all" ]]; then
|
||||
local vhostList
|
||||
run vhostList error openlitespeedConfigVhostList || { printRow; printDanger "Cannot get vhost list: $error"; return 1; }
|
||||
listContains "$target" "$vhostList" || { printRow; printDanger "Unknown domain: $target"; return 1; }
|
||||
uid=$(domainToUid "$target")
|
||||
[[ -n "$uid" ]] || { printDanger "Cannot resolve UID for: $target"; return 1; }
|
||||
fi
|
||||
|
||||
local pod phase
|
||||
while IFS='|' read -r pod phase; do
|
||||
printSection "$pod"
|
||||
|
||||
if [[ "$phase" != "Running" ]]; then
|
||||
printDotText "Phase" "$fontRed$phase$fontReset"
|
||||
else
|
||||
printDotText "Phase" "$fontGreen$phase$fontReset"
|
||||
|
||||
if [[ -n "$uid" ]]; then
|
||||
runSilent openlitespeedPodExec "$pod" pkill -u "$uid" -x lsphp
|
||||
else
|
||||
runSilent openlitespeedPodExec "$pod" pkill -x lsphp
|
||||
fi
|
||||
printDotText "kill$fontBlue lsphp$fontReset processes for $target" "$labelDone"
|
||||
fi
|
||||
done < <(awk 'NF' <<< "$podList")
|
||||
}
|
||||
|
||||
# Prints OpenLiteSpeed and PHP versions for each OLS pod.
|
||||
function cmdOpenlitespeedInfo() {
|
||||
local output error podList ver pid
|
||||
|
||||
if ! run podList error k3sPodListStatus "$olsKube"; then
|
||||
printDanger "Get pods: $error"
|
||||
elif [[ -z "$podList" ]]; then
|
||||
printDanger "Pods not found"
|
||||
else
|
||||
while IFS='|' read -r pod phase; do
|
||||
printSection "$pod"
|
||||
|
||||
if [[ "$phase" != "Running" ]]; then
|
||||
printDotText "Phase" "$fontRed$phase$fontReset"
|
||||
else
|
||||
printDotText "Phase" "$fontGreen$phase$fontReset"
|
||||
|
||||
if ! run output error openlitespeedPodExec "$pod" /usr/local/lsws/bin/lswsctrl status; then
|
||||
printDotText "Status" "$labelUnknown"
|
||||
printDanger "$error"
|
||||
elif [[ "$output" =~ "running" ]]; then
|
||||
printDotText "OpenLiteSpeed status" "$fontGreen$output$fontReset"
|
||||
else
|
||||
printDotText "OpenLiteSpeed status" "$fontRed$output$fontReset"
|
||||
fi
|
||||
|
||||
if run output error openlitespeedPodExec "$pod" /usr/local/lsws/bin/lshttpd -v; then
|
||||
ver=$(awk 'NR==1{print $1, $2}' <<< "$output")
|
||||
printDotText "OpenLiteSpeed version" "$ver"
|
||||
else
|
||||
printDotText "OpenLiteSpeed version" "$labelUnknown"
|
||||
printDanger "$error"
|
||||
fi
|
||||
|
||||
if run output error openlitespeedPodExec "$pod" php -r 'echo PHP_VERSION, "\n";'; then
|
||||
printDotText "PHP version" "$output"
|
||||
else
|
||||
printDotText "PHP version" "$labelUnknown"
|
||||
printDanger "$error"
|
||||
fi
|
||||
fi
|
||||
done < <(awk 'NF' <<< "$podList")
|
||||
fi
|
||||
|
||||
printSection "Hosts"
|
||||
local vhostList vhostDown
|
||||
if run output error openlitespeedConfigVhostList; then
|
||||
mapfile -t vhostList < <(awk 'NF' <<< "$output")
|
||||
printDotText "all" "${#vhostList[@]}"
|
||||
else
|
||||
printDotText "all" "$labelUnknown"
|
||||
printDanger "$error"
|
||||
fi
|
||||
|
||||
if run output error openlitespeedConfigVhostList "down"; then
|
||||
mapfile -t vhostDownList < <(awk 'NF' <<< "$output")
|
||||
printDotText "down" "${#vhostDownList[@]}"
|
||||
else
|
||||
printDotText "down" "$labelUnknown"
|
||||
printDanger "$error"
|
||||
fi
|
||||
|
||||
local count="$(find "$olsVhostsPath" -mindepth 1 -maxdepth 1 -type d | wc -l)"
|
||||
printDotText "directories" "$fontGray$olsVhostsPath$fontReset $count"
|
||||
}
|
||||
|
||||
# Marks a virtual host as suspended.
|
||||
# $1 (domain): site domain name.
|
||||
function cmdOpenlitespeedVhostDown() {
|
||||
printRow
|
||||
|
||||
local error
|
||||
if ! runError error openlitespeedVhostConfigDown "$@"; then
|
||||
printDotText "VHost down" "$labelFail"
|
||||
printDanger "$error"
|
||||
else
|
||||
printDotText "VHost down" "$labelPass"
|
||||
cmdOpenlitespeedRestart
|
||||
fi
|
||||
}
|
||||
|
||||
# Marks a virtual host as active.
|
||||
# $1 (domain): site domain name.
|
||||
function cmdOpenlitespeedVhostUp() {
|
||||
printRow
|
||||
|
||||
local error
|
||||
if ! runError error openlitespeedVhostConfigUp "$@"; then
|
||||
printDotText "VHost up" "$labelFail"
|
||||
printDanger "$error"
|
||||
else
|
||||
printDotText "VHost up" "$labelPass"
|
||||
cmdOpenlitespeedRestart
|
||||
fi
|
||||
}
|
||||
|
||||
# Lists virtual hosts with optional status filtering.
|
||||
# [$1] (type): all (default) | up | down.
|
||||
function cmdOpenlitespeedSiteList() {
|
||||
printRow
|
||||
|
||||
local output error type="${1:-all}"
|
||||
if ! run output error openlitespeedConfigVhostList "$type"; then
|
||||
printDanger "$error"
|
||||
else
|
||||
printText "$output"
|
||||
fi
|
||||
}
|
||||
|
||||
# Updates the Traefik middleware allowlist for OpenLiteSpeed WebAdmin.
|
||||
function cmdOpenlitespeedAdminWhiteList() {
|
||||
printRow
|
||||
|
||||
local output error
|
||||
run output error openlitespeedAdminWhiteList || { printDotText "Update" "$labelFail"; printDanger "$error"; return 1; }
|
||||
|
||||
printDotText "White list" "$output"
|
||||
printDotText "Update" "$labelDone"
|
||||
}
|
||||
|
||||
# Updates OpenLiteSpeed WebAdmin access control from current Traefik pod IPs.
|
||||
function cmdOpenlitespeedAdminAllowList() {
|
||||
printRow
|
||||
|
||||
local output error
|
||||
run output error openlitespeedAdminAllowList || { printDotText "Update" "$labelFail"; printDanger "$error"; return 1; }
|
||||
|
||||
printDotText "Allow list: ${output:-$labelNull}"
|
||||
printDotText "Update" "$labelDone"
|
||||
cmdOpenlitespeedRestart
|
||||
}
|
||||
|
||||
# Sets aliases for an OpenLiteSpeed virtual host.
|
||||
# $1 (domain): primary site domain name.
|
||||
# $@ (...): alias domain names.
|
||||
function cmdOpenlitespeedVhostAliasSet() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
|
||||
printRow
|
||||
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local vhostList aliasList output error
|
||||
if ! run vhostList error openlitespeedConfigVhostList; then
|
||||
appError "Error retrieving vhost list: $error"
|
||||
return 1
|
||||
elif ! listContains "$domain" "$vhostList"; then
|
||||
appError "Domain not exists in OpenLiteSpeed config: $domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
run output error openlitespeedVhostSet "$@" || {
|
||||
printDotText "Set" "$labelFail"
|
||||
printDanger "$error"
|
||||
return 1
|
||||
}
|
||||
|
||||
printDotText "Alias" "${output:-$labelNull}"
|
||||
printDotText "Set" "$labelDone"
|
||||
cmdOpenlitespeedRestart
|
||||
}
|
||||
|
||||
# Lists aliases for a domain or all domains.
|
||||
# [$1] (target): domain name, or "all" to list all.
|
||||
function cmdOpenlitespeedAliasList() {
|
||||
printRow
|
||||
|
||||
local output error domain target="$1"
|
||||
if [[ "$target" == all ]]; then
|
||||
if ! run output error openlitespeedConfigAliasList; then
|
||||
printDanger "$error"
|
||||
elif [[ -z "$output" ]]; then
|
||||
printText "$labelNull"
|
||||
else
|
||||
printText "$output"
|
||||
fi
|
||||
else
|
||||
domain=$(domainPrepare "$target")
|
||||
if ! run output error openlitespeedConfigVhostAliasList "$domain"; then
|
||||
printDanger "$error"
|
||||
elif [[ -z "$output" ]]; then
|
||||
printText "$labelNull"
|
||||
else
|
||||
printText "$output"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Tests the OpenLiteSpeed configuration inside the container.
|
||||
function cmdOpenlitespeedConfigCheck() {
|
||||
printRow
|
||||
|
||||
local output error
|
||||
run output error openlitespeedExec sh -lc "/usr/local/lsws/bin/openlitespeed -t 2>/dev/null || true"
|
||||
if grep -qi 'configuration failed!' <<< "$output"; then
|
||||
printDotText "Check config" "$labelFail"
|
||||
printDanger "$output"
|
||||
else
|
||||
printDotText "Check config" "$labelPass"
|
||||
fi
|
||||
}
|
||||
|
||||
function cmdOpenlitespeedVhostRebuild() {
|
||||
local target="$1"
|
||||
local vhostList error
|
||||
|
||||
printRow
|
||||
|
||||
if [[ -z "$target" ]]; then
|
||||
printDanger "Target not specified";
|
||||
elif ! run vhostList error openlitespeedConfigVhostList; then
|
||||
printDanger "Cannot get vhost list: $error";
|
||||
elif [[ "$target" == "all" ]]; then
|
||||
local domain
|
||||
for domain in $vhostList; do
|
||||
if ! runError error openlitespeedVhostRebuild "$domain"; then
|
||||
printDotText "$domain" "$labelFail"
|
||||
printDanger "$error"
|
||||
else
|
||||
printDotText "$domain" "$labelDone"
|
||||
fi
|
||||
done
|
||||
elif ! listContains "$target" "$vhostList"; then
|
||||
printDanger "Unknown domain: $target";
|
||||
elif ! runError error openlitespeedVhostRebuild "$target"; then
|
||||
printDotText "$target" "$labelFail"
|
||||
printDanger "$error"
|
||||
else
|
||||
printDotText "$target" "$labelDone"
|
||||
fi
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
postfixLabel="[Postfix]"
|
||||
|
||||
# Generates a self-signed TLS certificate for Postfix if one does not already exist.
|
||||
function cmdPostfixPreparation() {
|
||||
printSection "Preparation..."
|
||||
|
||||
if [[ ! -f "$postfixTlsCrtFile" || ! -f "$postfixTlsKeyFile" ]]; then
|
||||
local crtPath; crtPath=$(dirname "$postfixTlsCrtFile")
|
||||
local tlsCnfFile="$crtPath/openssl.cnf"
|
||||
local cn="${postfixHost:-$postfixDomain}"
|
||||
local sanList="DNS:${cn}"
|
||||
[[ -n "$postfixDomain" ]] && sanList="${sanList},DNS:${postfixDomain}"
|
||||
mkdir -p "$crtPath" || {
|
||||
printDotText "preparation" "$labelFail"
|
||||
printDanger "Failed to create folder for certificate";
|
||||
return 1;
|
||||
}
|
||||
|
||||
cat >"$tlsCnfFile" <<EOF
|
||||
[req]
|
||||
distinguished_name = dn
|
||||
x509_extensions = v3_req
|
||||
prompt = no
|
||||
[dn]
|
||||
CN = ${cn}
|
||||
[v3_req]
|
||||
subjectAltName = ${sanList}
|
||||
keyUsage = digitalSignature, keyEncipherment
|
||||
extendedKeyUsage = serverAuth
|
||||
EOF
|
||||
openssl req -x509 -nodes -newkey rsa:2048 -days 365 -keyout "$postfixTlsKeyFile" -out "$postfixTlsCrtFile" -config "$tlsCnfFile" || {
|
||||
printDotText "preparation" "$labelFail"
|
||||
printDanger "TLS gen failed";
|
||||
return 1;
|
||||
}
|
||||
fi
|
||||
|
||||
printDotText "preparation" "$labelDone"
|
||||
}
|
||||
|
||||
# Renders the Postfix Kubernetes YAML manifest via Helm.
|
||||
function cmdPostfixYamlRender() {
|
||||
local templateFile="templates/$postfixKube.yaml"
|
||||
|
||||
printSection "Render YAML file | Helm"
|
||||
printDotText "Template" "$appAssetsPath/k3s/$templateFile"
|
||||
[[ -f "$appAssetsPath/k3s/$templateFile" ]] || {
|
||||
printDanger "Template file not found"
|
||||
return 1
|
||||
}
|
||||
|
||||
local val postfixTlsCrtB64 postfixTlsKeyB64
|
||||
val=$(base64 -w0 "$postfixTlsCrtFile") || {
|
||||
printDotText "render" "$labelFail"
|
||||
printDanger "Base64 gen failed (1)"
|
||||
return 1
|
||||
}
|
||||
postfixTlsCrtB64=$(sed 's/[&\\]/\\&/g' <<<"$val") || {
|
||||
printDotText "render" "$labelFail"
|
||||
printDanger "Base64 gen failed (2)"
|
||||
return 1
|
||||
}
|
||||
val=$(base64 -w0 "$postfixTlsKeyFile") || {
|
||||
printDotText "render" "$labelFail"
|
||||
printDanger "Base64 gen failed (3)"
|
||||
return 1
|
||||
}
|
||||
postfixTlsKeyB64=$(sed 's/[&\\]/\\&/g' <<<"$val") || {
|
||||
printDotText "render" "$labelFail"
|
||||
printDanger "Base64 gen failed (4)"
|
||||
return 1
|
||||
}
|
||||
|
||||
local varsFile
|
||||
varsFile=$(mktemp "/tmp/$postfixKube.vars.XXXXXX.yaml") || {
|
||||
printDotText "render" "$labelFail"
|
||||
printDanger "Create temp variables file"
|
||||
return 1
|
||||
}
|
||||
printDotText "Variables" "$varsFile"
|
||||
trap 'rm -f -- "$varsFile"' RETURN
|
||||
cat > "$varsFile" <<EOF
|
||||
postfixHelm: true
|
||||
namespace: $k3sNamespace
|
||||
postfix: $postfixKube
|
||||
postfixPath: $postfixPath
|
||||
postfixTlsCrtB64: $postfixTlsCrtB64
|
||||
postfixTlsKeyB64: $postfixTlsKeyB64
|
||||
postfixHost: $postfixHost
|
||||
postfixPostmaster: $postfixPostmaster
|
||||
postfixDefaultRealm: $postfixDefaultRealm
|
||||
postfixConfigPath: $postfixConfigPath
|
||||
postfixDkimPath: $postfixDkimPath
|
||||
postfixDkimSelector: $postfixDkimSelector
|
||||
postfixDomainsFile: $postfixDomainsFile
|
||||
postfixAliasesFile: $postfixAliasesFile
|
||||
postfixSendersFile: $postfixSendersFile
|
||||
EOF
|
||||
|
||||
printDotText "Result" "$postfixYaml"
|
||||
mkdir -p -- "$(dirname -- "$postfixYaml")" || return 1
|
||||
fileBackup "$postfixYaml"
|
||||
helm template stack "$appAssetsPath/k3s" -f "$varsFile" --show-only "$templateFile" > "$postfixYaml" || {
|
||||
printDotText "render" "$labelFail"
|
||||
printDanger "Render failed"
|
||||
return 1
|
||||
}
|
||||
|
||||
printDotText "render" "$labelDone"
|
||||
}
|
||||
|
||||
# Initializes Postfix after install: generates DKIM for postfixHost and sets sasldb2 ownership.
|
||||
function cmdPostfixInit() {
|
||||
printSection "Initialization..."
|
||||
|
||||
touch "$postfixConfigPath/$postfixDomainsFile" || return 1
|
||||
touch "$postfixConfigPath/$postfixAliasesFile" || return 1
|
||||
touch "$postfixConfigPath/$postfixSendersFile" || return 1
|
||||
|
||||
postfixDkimAdd "$postfixHost" || {
|
||||
printDotText "Add DKIM for host" "$labelFail"
|
||||
return 1
|
||||
}
|
||||
printDotText "Add DKIM for host" "$labelDone"
|
||||
|
||||
local error
|
||||
if ! runError error postfixExec chown postfix:postfix /config/sasldb2; then
|
||||
printDotText "Set owner /config/sasldb2" "$labelFail"
|
||||
printDanger "$error"
|
||||
return 1
|
||||
fi
|
||||
printDotText "Set owner /config/sasldb2" "$labelDone"
|
||||
}
|
||||
|
||||
function cmdPostfixUpdate() {
|
||||
cmdPostfixYamlRender || return 1
|
||||
cmdK3sYamlApplyEx "$postfixYaml" || return 1
|
||||
}
|
||||
|
||||
# Installs Postfix into Kubernetes.
|
||||
function cmdPostfixInstall() {
|
||||
cmdPostfixPreparation || return 1
|
||||
cmdPostfixYamlRender || return 1
|
||||
cmdK3sYamlApplyEx "$postfixYaml" || return 1
|
||||
cmdPostfixInit || return 1
|
||||
}
|
||||
|
||||
# Uninstalls Postfix Kubernetes resources.
|
||||
function cmdPostfixUninstall() {
|
||||
"$k3sCmd" kubectl delete -f "$postfixYaml"
|
||||
}
|
||||
|
||||
function cmdPostfixUser() {
|
||||
local output error
|
||||
|
||||
printRow
|
||||
|
||||
if ! run output error postfixUserList; then
|
||||
printDanger "$error"
|
||||
elif [[ -z "$output" ]]; then
|
||||
printText "$labelNull"
|
||||
else
|
||||
printText "$output"
|
||||
fi
|
||||
}
|
||||
|
||||
# Prints the Postfix mail version.
|
||||
function cmdPostfixInfo() {
|
||||
local output error podList ver pid
|
||||
|
||||
if ! run podList error k3sPodListStatus "$postfixKube"; then
|
||||
printDanger "Get pods: $error"
|
||||
elif [[ -z "$podList" ]]; then
|
||||
printDanger "Pods not found"
|
||||
else
|
||||
while IFS='|' read -r pod phase; do
|
||||
printSection "$pod"
|
||||
|
||||
if [[ "$phase" != "Running" ]]; then
|
||||
printDotText "Phase" "$fontRed$phase$fontReset"
|
||||
else
|
||||
printDotText "Phase" "$fontGreen$phase$fontReset"
|
||||
|
||||
if ! run output error postfixPodExec "$pod" postfix status; then
|
||||
printDotText "Postfix status" "$fontRed$labelFail$fontReset"
|
||||
printDanger "$error"
|
||||
else
|
||||
output="${output:-$error}"
|
||||
if [[ $output == *"is running"* ]]; then
|
||||
pid=${output##*PID: }
|
||||
pid=${pid%%[^0-9]*}
|
||||
printDotText "Postfix status" "$labelRunning"
|
||||
printDotText "pid" "$pid"
|
||||
else
|
||||
printDotText "Postfix status" "$fontRed$output$fontReset"
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! run output error postfixPodExec "$pod" postconf mail_version; then
|
||||
printDotText "Postfix mail version" "$fontRed$labelFail$fontReset"
|
||||
printDanger "$error"
|
||||
else
|
||||
ver=$(printf '%s' "$output" | cut -d '=' -f2 | tr -d '\r\n')
|
||||
printDotText "Postfix mail version" "$ver"
|
||||
fi
|
||||
fi
|
||||
done < <(awk 'NF' <<< "$podList")
|
||||
fi
|
||||
}
|
||||
|
||||
# Checks MTA host DNS records (A, SPF, PTR, DKIM) against configured values.
|
||||
function cmdPostfixMtaDnsCheck() {
|
||||
local label output error text
|
||||
|
||||
printSection "MTA DNS: $postfixHost"
|
||||
|
||||
# A record
|
||||
if ! run output error dig +short A "$postfixHost" "$dnsResolver"; then
|
||||
printDotText "A record" "$fontRed${output:-$error}$fontReset"
|
||||
else
|
||||
text=$(printf '%s' "$output" | paste -sd, - | sed 's/,/ \/ /g')
|
||||
if grep -Fxq -- "$postfixIp" <<<"$output"; then
|
||||
printDotText "A record" "$fontGreen$text$fontReset"
|
||||
else
|
||||
printDotText "A record" "$fontYellow$text$fontReset"
|
||||
fi
|
||||
fi
|
||||
|
||||
# SPF record
|
||||
if ! run output error dig +short TXT "$postfixHost" "$dnsResolver"; then
|
||||
printDotText "SPF record" "$fontRed${output:-$error}$fontReset"
|
||||
elif grep -Eq '^"?v=spf1([[:space:]]|")' <<<"$output"; then
|
||||
printDotText "SPF record" "$fontGreen$output$fontReset"
|
||||
else
|
||||
printDotText "SPF record" "$fontRed$output$fontReset"
|
||||
fi
|
||||
|
||||
# PTR record
|
||||
if ! run output error dig -x "$postfixIp" +short "$dnsResolver"; then
|
||||
printDotText "PTR record" "$fontRed${output:-$error}$fontReset"
|
||||
else
|
||||
text=$(printf '%s' "$output" | paste -sd, - | sed 's/,/ \/ /g')
|
||||
if grep -Fxq -- "$postfixHost." <<<"$output"; then
|
||||
printDotText "PTR record" "$fontGreen$text$fontReset"
|
||||
else
|
||||
printDotText "PTR record" "$fontYellow$text$fontReset"
|
||||
fi
|
||||
fi
|
||||
|
||||
# DKIM record
|
||||
label="$postfixLabel DKIM record: $postfixHost"
|
||||
if ! run output error dig +short TXT "$postfixDkimSelector._domainkey.$postfixHost"; then
|
||||
printDotText "DKIM record" "$fontRed${output:-$error}$fontReset"
|
||||
else
|
||||
local dkimDnsNorm dkimFileRaw dkimFileNorm
|
||||
dkimDnsNorm=$(
|
||||
printf '%s\n' "$output" |
|
||||
tr -d '\n' |
|
||||
sed -e 's/"//g' -e 's/[[:space:]]//g' |
|
||||
sed -n 's/.*p=\([^;]*\).*/\1/p'
|
||||
)
|
||||
dkimFileRaw=$(postfixDkimGet "$postfixHost")
|
||||
dkimFileNorm=$(
|
||||
printf '%s\n' "$dkimFileRaw" |
|
||||
tr -d '\n' |
|
||||
sed -e 's/[()"]//g' -e 's/[[:space:]]//g' |
|
||||
sed -n 's/.*p=\([^;]*\).*/\1/p'
|
||||
)
|
||||
if [[ "$dkimDnsNorm" == "$dkimFileNorm" ]]; then
|
||||
printText "$fontGreen$dkimDnsNorm$fontReset"
|
||||
else
|
||||
printText "DNS : $fontYellow$dkimDnsNorm$fontReset"
|
||||
printText "File: $fontYellow$dkimFileNorm$fontReset"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
@@ -0,0 +1,425 @@
|
||||
redisLabel="[Redis]"
|
||||
redisSentinelLabel="[RedisSentinel]"
|
||||
redisHaproxyLabel="[RedisHAProxy]"
|
||||
|
||||
# Prepares Kubernetes secrets for Redis.
|
||||
function cmdRedisPreparation() {
|
||||
printSection "Preparation..."
|
||||
|
||||
local error
|
||||
runError error k3sRun delete secret "$redisKube-secret" --ignore-not-found || {
|
||||
printDotText "preparation" "$labelFail"
|
||||
printDanger "Delete old Secret: $error"
|
||||
return 1
|
||||
}
|
||||
|
||||
runError error k3sRun create secret generic "$redisKube-secret" --from-literal=root-password="$redisRootPass" || {
|
||||
printDotText "preparation" "$labelFail"
|
||||
printDanger "Create new Secret: $error"
|
||||
return 1
|
||||
}
|
||||
|
||||
fileBackup "$redisPath/$redisFileUsersAcl"
|
||||
|
||||
printDotText "preparation" "$labelDone"
|
||||
}
|
||||
|
||||
# Renders the Redis Kubernetes YAML manifest via Helm.
|
||||
function cmdRedisYamlRender() {
|
||||
local templateFile="templates/$redisKube.yaml"
|
||||
|
||||
printSection "Render YAML file | Helm"
|
||||
printDotText "Template" "$appAssetsPath/k3s/$templateFile"
|
||||
[[ -f "$appAssetsPath/k3s/$templateFile" ]] || {
|
||||
printDanger "Template file not found"
|
||||
return 1
|
||||
}
|
||||
|
||||
local profileCpuFile="$appAssetsPath/k3s/profiles/cpu-$kubeCpuProfile.yaml"
|
||||
printDotText "CPU profile" "$profileCpuFile"
|
||||
[[ -f "$profileCpuFile" ]] || {
|
||||
printDanger "CPU profile file not found"
|
||||
return 1
|
||||
}
|
||||
|
||||
local profileMemoryFile="$appAssetsPath/k3s/profiles/memory-$kubeMemoryProfile.yaml"
|
||||
printDotText "Memory profile" "$profileMemoryFile"
|
||||
[[ -f "$profileMemoryFile" ]] || {
|
||||
printDanger "Memory profile file not found"
|
||||
return 1
|
||||
}
|
||||
|
||||
local varsFile
|
||||
varsFile=$(mktemp "/tmp/$redisKube.vars.XXXXXX.yaml") || {
|
||||
printDotText "render" "$labelFail"
|
||||
printDanger "Create temp variables file"
|
||||
return 1
|
||||
}
|
||||
printDotText "Variables" "$varsFile"
|
||||
trap 'rm -f -- "$varsFile"' RETURN
|
||||
cat > "$varsFile" <<EOF
|
||||
redisHelm: true
|
||||
namespace: $k3sNamespace
|
||||
redis: $redisKube
|
||||
redisSentinel: $redisSentinelKube
|
||||
redisPath: $redisPath
|
||||
redisFileUsersAcl: $redisFileUsersAcl
|
||||
EOF
|
||||
|
||||
printDotText "Result" "$redisYaml"
|
||||
mkdir -p -- "$(dirname -- "$redisYaml")" || return 1
|
||||
fileBackup "$redisYaml"
|
||||
helm template stack "$appAssetsPath/k3s" -f "$varsFile" -f "$profileCpuFile" -f "$profileMemoryFile" --show-only "$templateFile" > "$redisYaml" || {
|
||||
printDotText "render" "$labelFail"
|
||||
printDanger "Render failed"
|
||||
return 1
|
||||
}
|
||||
|
||||
printDotText "render" "$labelDone"
|
||||
}
|
||||
|
||||
# Updates Redis Kubernetes resources (limits, config, etc.) without re-initialization.
|
||||
function cmdRedisUpdate() {
|
||||
cmdRedisYamlRender || return 1
|
||||
cmdK3sYamlApplyEx "$redisYaml" || return 1
|
||||
}
|
||||
|
||||
# Installs Redis into Kubernetes.
|
||||
function cmdRedisInstall() {
|
||||
cmdRedisPreparation || return 1
|
||||
cmdRedisYamlRender || return 1
|
||||
cmdK3sYamlApplyEx "$redisYaml" || return 1
|
||||
}
|
||||
|
||||
# Uninstalls Redis Kubernetes resources.
|
||||
function cmdRedisUninstall() {
|
||||
"$k3sCmd" kubectl delete -f "$redisYaml"
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
# local output error podList ver pid
|
||||
|
||||
# if ! run podList error k3sPodListStatus "$olsKube"; then
|
||||
# printDanger "Get pods: $error"
|
||||
# elif [[ -z "$podList" ]]; then
|
||||
# printDanger "Pods not found"
|
||||
# else
|
||||
# while IFS='|' read -r pod phase; do
|
||||
# printSection "$pod"
|
||||
|
||||
# if [[ "$phase" != "Running" ]]; then
|
||||
# printDotText "Phase" "$fontRed$phase$fontReset"
|
||||
# else
|
||||
# printDotText "Phase" "$fontGreen$phase$fontReset"
|
||||
|
||||
# if ! run output error openlitespeedPodExec "$pod" /usr/local/lsws/bin/lswsctrl status; then
|
||||
# printDotText "Status" "$labelUnknown"
|
||||
# printDanger "$error"
|
||||
# elif [[ "$output" =~ "running" ]]; then
|
||||
# printDotText "OpenLiteSpeed status" "$fontGreen$output$fontReset"
|
||||
# else
|
||||
# printDotText "OpenLiteSpeed status" "$fontRed$output$fontReset"
|
||||
# fi
|
||||
|
||||
# if run output error openlitespeedPodExec "$pod" /usr/local/lsws/bin/lshttpd -v; then
|
||||
# ver=$(awk 'NR==1{print $1, $2}' <<< "$output")
|
||||
# printDotText "OpenLiteSpeed version" "$ver"
|
||||
# else
|
||||
# printDotText "OpenLiteSpeed version" "$labelUnknown"
|
||||
# printDanger "$error"
|
||||
# fi
|
||||
|
||||
# if run output error openlitespeedPodExec "$pod" php -r 'echo PHP_VERSION, "\n";'; then
|
||||
# printDotText "PHP version" "$output"
|
||||
# else
|
||||
# printDotText "PHP version" "$labelUnknown"
|
||||
# printDanger "$error"
|
||||
# fi
|
||||
# fi
|
||||
# done < <(awk 'NF' <<< "$podList")
|
||||
# fi
|
||||
|
||||
# printRow
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Prints replication INFO for all Redis and Sentinel pods.
|
||||
function cmdRedisInfo() {
|
||||
local output error podList ver pid
|
||||
|
||||
if ! run podList error k3sPodListStatus "$redisKube"; then
|
||||
printDanger "Get pods: $error"
|
||||
elif [[ -z "$podList" ]]; then
|
||||
printDanger "Pods not found"
|
||||
else
|
||||
while IFS='|' read -r pod phase; do
|
||||
printSection "$pod"
|
||||
|
||||
if [[ "$phase" != "Running" ]]; then
|
||||
printDotText "Phase" "$fontRed$phase$fontReset"
|
||||
else
|
||||
printDotText "Phase" "$fontGreen$phase$fontReset"
|
||||
|
||||
if ! run output error redisPodExecCli "$pod" INFO server; then
|
||||
printDotText "Redis version" "$labelUnknown"
|
||||
printDanger "$error"
|
||||
else
|
||||
ver=$(printf '%s' "$output" | grep redis_version | cut -d ':' -f2 | tr -d '[:space:]')
|
||||
printDotText "Redis version" "$ver"
|
||||
fi
|
||||
|
||||
if ! run output error redisPodExecCli "$pod" INFO replication; then
|
||||
printDotText "Redis version" "$labelUnknown"
|
||||
printDanger "$error"
|
||||
else
|
||||
|
||||
local role slaves masterHost masterLinkStatus slaveLag
|
||||
role=$(printf '%s' "$output" | grep -i "role" | cut -d: -f2 | tr -d '\r\n')
|
||||
if [[ "$role" == "master" ]]; then
|
||||
printDotText "Replica role" "$fontGreen$role$fontReset"
|
||||
|
||||
slaves=$(printf '%s' "$output" | grep -i "connected_slaves" | cut -d: -f2 | tr -d '\r\n')
|
||||
if [[ "$slaves" -eq 0 ]]; then
|
||||
printDotText "Slaves" "$fontRed$slaves$fontReset"
|
||||
else
|
||||
printDotText "Slaves" "$fontGreen$slaves$fontReset"
|
||||
fi
|
||||
elif [[ "$role" == "slave" ]]; then
|
||||
printDotText "Replica role" "$fontGreen$role$fontReset"
|
||||
|
||||
masterHost=$(printf '%s' "$output" | grep -i "master_host" | cut -d: -f2 | tr -d '\r\n')
|
||||
masterLinkStatus=$(printf '%s' "$output" | grep -i "master_link_status" | cut -d: -f2 | tr -d '\r\n')
|
||||
if [[ -z "$masterHost" || "$masterLinkStatus" != "up" ]]; then
|
||||
[[ -z "$masterHost" ]] && printDotText "Master" "${fontRed}Not connect to master (master_host)$fontReset"
|
||||
[[ "$masterLinkStatus" != "up" ]] && printDotText "Master" "${fontRed}Not connect to master (master_link_status)$fontReset"
|
||||
continue
|
||||
fi
|
||||
printDotText "Master" "$fontGreen$masterHost$fontReset"
|
||||
|
||||
slaveLag=$(printf '%s' "$output" | grep -i "master_last_io_seconds_ago" | cut -d: -f2 | tr -d '\r\n')
|
||||
[[ "$slaveLag" -ge 3 ]] && printDotText "Replication delay" "$fontYallow${slaveLag}s$fontReset"
|
||||
else
|
||||
printDotText "Replica role" "$fontRed$role$fontReset"
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
|
||||
fi
|
||||
done < <(awk 'NF' <<< "$podList")
|
||||
fi
|
||||
|
||||
if ! run podList error k3sPodListStatus "$redisSentinelKube"; then
|
||||
printDanger "Get pods: $error"
|
||||
elif [[ -z "$podList" ]]; then
|
||||
printDanger "Pods not found"
|
||||
else
|
||||
local masterInfo slaveInfo
|
||||
local masterName masterIP masterPort masterNumOtherSentinels masterQuorum
|
||||
local masterSig replicaSig refPod refMasterSig refReplicaSig
|
||||
local activeReplicaCount mismatch=0
|
||||
|
||||
while IFS='|' read -r pod phase; do
|
||||
printSection "$pod"
|
||||
|
||||
if [[ "$phase" != "Running" ]]; then
|
||||
printDotText "Phase" "$fontRed$phase$fontReset"
|
||||
else
|
||||
printDotText "Phase" "$fontGreen$phase$fontReset"
|
||||
|
||||
if ! run output error redisPodExecCli "$pod" INFO server; then
|
||||
printDotText "RedisSentinel version" "$labelUnknown"
|
||||
printDanger "$error"
|
||||
else
|
||||
ver=$(printf '%s' "$output" | grep redis_version | cut -d ':' -f2 | tr -d '[:space:]')
|
||||
printDotText "RedisSentinel version" "$ver"
|
||||
fi
|
||||
|
||||
run masterInfo error redisPodExecCli "$pod" SENTINEL masters || {
|
||||
printDotText "Get sentinel masters" "$labelFail"
|
||||
printDanger "$error"
|
||||
mismatch=1
|
||||
continue
|
||||
}
|
||||
|
||||
local -A masterData=()
|
||||
while read -r key && read -r value; do
|
||||
masterData["$key"]="$value"
|
||||
done <<< "$masterInfo"
|
||||
masterName="${masterData[name]}"
|
||||
masterIP="${masterData[ip]}"
|
||||
masterPort="${masterData[port]}"
|
||||
masterNumOtherSentinels="${masterData[num-other-sentinels]:-0}"
|
||||
masterQuorum="${masterData[quorum]:-0}"
|
||||
|
||||
run slaveInfo error redisPodExecCli "$pod" --raw sentinel replicas "$masterName" || {
|
||||
printDotText "Get sentinel replicas" "$labelFail"
|
||||
printDanger "$error"
|
||||
mismatch=1
|
||||
continue
|
||||
}
|
||||
|
||||
replicaSig="$(redisSentinelParseReplicas "$slaveInfo" | awk 'NF' | sort)"
|
||||
activeReplicaCount="$(awk 'NF {c++} END {print c+0}' <<< "$replicaSig")"
|
||||
masterSig="${masterName}|${masterIP}|${masterPort}|${activeReplicaCount}|${masterNumOtherSentinels}|${masterQuorum}"
|
||||
|
||||
refPod="" refMasterSig="" refReplicaSig=""
|
||||
if [[ -z "$refPod" ]]; then
|
||||
refPod="$pod"
|
||||
refMasterSig="$masterSig"
|
||||
refReplicaSig="$replicaSig"
|
||||
fi
|
||||
|
||||
if [[ "$masterSig" != "$refMasterSig" || "$replicaSig" != "$refReplicaSig" ]]; then
|
||||
mismatch=1
|
||||
printDotText "Topology" "mismatch vs $fontRed$refPod$fontReset"
|
||||
# else
|
||||
# printDotText "Topology" "matches $fontGreen$refPod$fontReset"
|
||||
fi
|
||||
|
||||
printDotText " ┌ Replica" "$masterName"
|
||||
if (( masterQuorum < 1 )); then
|
||||
printDotText " ├ Quorum" "$fontRed$masterQuorum$fontReset"
|
||||
else
|
||||
printDotText " ├ Quorum" "$fontGreen$masterQuorum$fontReset"
|
||||
fi
|
||||
printDotText " ├ Other sentinels" "$masterNumOtherSentinels"
|
||||
|
||||
# printDotText "Master" "$masterIP:$masterPort"
|
||||
printDotText " ├ Master" "$masterIP"
|
||||
if (( activeReplicaCount < 1 )); then
|
||||
printDotText " └ Slave count" "$fontRed$activeReplicaCount$fontReset"
|
||||
else
|
||||
printDotText " └ Slave count" "$fontGreen$activeReplicaCount$fontReset"
|
||||
fi
|
||||
while IFS=$'\t' read -r slaveName slaveIP slavePort slaveMaster slaveRunId; do
|
||||
[[ -z "$slaveName" ]] && continue
|
||||
printText " ┊"
|
||||
printDotText " ├ Slave" "$slaveIP"
|
||||
printDotText " ├ Master" "$slaveMaster"
|
||||
printDotText " └ RunID" "$slaveRunId"
|
||||
done <<< "$replicaSig"
|
||||
fi
|
||||
done < <(awk 'NF' <<< "$podList")
|
||||
fi
|
||||
|
||||
if ! run podList error k3sPodListStatus "$redisKube-haproxy"; then
|
||||
printDanger "Get pods: $error"
|
||||
elif [[ -z "$podList" ]]; then
|
||||
printDanger "Pods not found"
|
||||
else
|
||||
while IFS='|' read -r pod phase; do
|
||||
printSection "$pod"
|
||||
|
||||
if [[ "$phase" != "Running" ]]; then
|
||||
printDotText "Phase" "$fontRed$phase$fontReset"
|
||||
else
|
||||
printDotText "Phase" "$fontGreen$phase$fontReset"
|
||||
|
||||
if ! run output error k3sRun exec "pod/$pod" -- haproxy -v; then
|
||||
printDotText "HAProxy version" "$labelUnknown"
|
||||
printDanger "$error"
|
||||
else
|
||||
ver=$(awk 'NR==1{print $3}' <<< "$output")
|
||||
printDotText "HAProxy version" "$ver"
|
||||
fi
|
||||
|
||||
local role
|
||||
if ! run output error redisExecCli -h redis-master -p 6379 ROLE; then
|
||||
printDotText "Replica role" "$labelFail"
|
||||
printDanger "$error"
|
||||
else
|
||||
role=$(printf '%s' "$output" | head -n1 | tr -d '\r\n')
|
||||
if [[ "$role" != "master" ]]; then
|
||||
printDotText "Replica role" "$fontRed$role$fontReset"
|
||||
else
|
||||
printDotText "Replica role" "$fontGreen$role$fontReset"
|
||||
fi
|
||||
|
||||
fi
|
||||
fi
|
||||
done < <(awk 'NF' <<< "$podList")
|
||||
fi
|
||||
}
|
||||
|
||||
# Checks replication role/health on each Redis pod and Sentinel topology consistency.
|
||||
function cmdRedisStatus() {
|
||||
local output error podList
|
||||
|
||||
if ! run podList error k3sPodList "$redisKube"; then
|
||||
printDanger "$redisLabel Get pods: $error"
|
||||
elif [[ -z "$podList" ]]; then
|
||||
printDanger "$redisLabel Pods not found"
|
||||
else
|
||||
while read -r pod; do
|
||||
printSection "$pod"
|
||||
if run output error redisPodExecCli "$pod" INFO replication; then
|
||||
printText "$output"
|
||||
else
|
||||
printDanger "$redisLabel $pod | $error"
|
||||
fi
|
||||
done < <(awk 'NF' <<< "$podList")
|
||||
fi
|
||||
|
||||
if ! run podList error k3sPodList "$redisSentinelKube"; then
|
||||
printDanger "$redisSentinelLabel Get pods: $error"
|
||||
elif [[ -z "$podList" ]]; then
|
||||
printDanger "$redisSentinelLabel Pods not found"
|
||||
else
|
||||
while read -r pod; do
|
||||
printSection "$pod"
|
||||
if run output error redisPodExecCli "$pod" INFO sentinel; then
|
||||
printText "$output"
|
||||
else
|
||||
printDanger "$redisSentinelLabel $pod | $error"
|
||||
fi
|
||||
done < <(awk 'NF' <<< "$podList")
|
||||
fi
|
||||
}
|
||||
|
||||
# Lists all Redis ACL users.
|
||||
function cmdRedisUser() {
|
||||
local output error
|
||||
|
||||
printRow
|
||||
|
||||
if ! run output error redisUserListGet; then
|
||||
printDanger "$error"
|
||||
elif [[ -z "$output" ]]; then
|
||||
printText "$labelNull"
|
||||
else
|
||||
printText "$output"
|
||||
fi
|
||||
}
|
||||
|
||||
# Flushes all keys from the Redis database.
|
||||
function cmdRedisDatabaseFlush() {
|
||||
local output error
|
||||
|
||||
printRow
|
||||
|
||||
if run output error redisDatabaseFlush; then
|
||||
printText "$output"
|
||||
else
|
||||
printDanger "$error"
|
||||
fi
|
||||
}
|
||||
|
||||
# Regenerates and applies the Redis root password across all nodes.
|
||||
function cmdRedisRootPassUpdate() {
|
||||
local output error
|
||||
|
||||
printRow
|
||||
|
||||
if run output error redisRootPassUpdate; then
|
||||
printText "$output"
|
||||
else
|
||||
printDanger "$error"
|
||||
fi
|
||||
}
|
||||
@@ -0,0 +1,473 @@
|
||||
restoreLabel="[Restore]"
|
||||
|
||||
# Restores site files from an archive.
|
||||
#
|
||||
# The archive is first extracted into a temporary directory located on the same
|
||||
# filesystem as the target vhost directory. After successful extraction, the
|
||||
# current vhost directory is moved to a temporary backup path and the restored
|
||||
# directory is moved into place.
|
||||
#
|
||||
# $1 (domain): Site domain name.
|
||||
# $2 (file): Source archive file path.
|
||||
#
|
||||
# Returns:
|
||||
# 0 on success, 1 on validation or restore failure.
|
||||
function restoreSiteFiles() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local target="$olsVhostsPath/$domain"
|
||||
|
||||
local source="$2"
|
||||
if [[ -z "$source" ]]; then
|
||||
appError "Source files not specified"
|
||||
return 1
|
||||
fi
|
||||
if [[ ! -e "$source" ]]; then
|
||||
appError "Source files not found: $source"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local tmpDir restoreSource output rc
|
||||
if [[ -f "$source" ]]; then
|
||||
tmpDir=$(mktemp -d) || {
|
||||
appError "Failed to create temporary restore directory"
|
||||
return 1
|
||||
}
|
||||
|
||||
archiveExtract "$source" "$tmpDir" || {
|
||||
rm -rf -- "$tmpDir" &>/dev/null
|
||||
return 1
|
||||
}
|
||||
|
||||
restoreSource="$tmpDir"
|
||||
else
|
||||
restoreSource="$source"
|
||||
fi
|
||||
|
||||
rm -rf -- "$target" || {
|
||||
[[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null
|
||||
appError "Failed to remove target directory: $target"
|
||||
return 1
|
||||
}
|
||||
|
||||
mkdir -p -- "$target" || {
|
||||
[[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null
|
||||
appError "Failed to create target directory: $target"
|
||||
return 1
|
||||
}
|
||||
|
||||
output=$(cp -a -- "$restoreSource"/. "$target/" 2>&1)
|
||||
rc=$?
|
||||
if [[ $rc -ne 0 ]]; then
|
||||
[[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null
|
||||
appError "Failed to copy restored files: $output"
|
||||
return 1
|
||||
fi
|
||||
|
||||
[[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null
|
||||
|
||||
openlitespeedVhostRebuild "$domain" || return 1
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Restores a site database from a SQL file or archive.
|
||||
#
|
||||
# If the source file is an archive, it is extracted into a temporary directory
|
||||
# first and must contain exactly one SQL file. If the target database already
|
||||
# exists, the SQL file is first imported into a temporary database to validate
|
||||
# the restore before recreating the target database.
|
||||
#
|
||||
# $1 (domain): Site domain name.
|
||||
# $2 (file): Source SQL file or archive file path.
|
||||
#
|
||||
# Returns:
|
||||
# 0 on success, 1 on validation or restore failure.
|
||||
function restoreSiteDatabase() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local file="$2"
|
||||
if [[ -z "$file" ]]; then
|
||||
appError "Source database file not specified"
|
||||
return 1
|
||||
fi
|
||||
if [[ ! -f "$file" ]]; then
|
||||
appError "Source database file not found: $file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local tmpDir importFile
|
||||
importFile="$file"
|
||||
|
||||
case "$file" in
|
||||
*.zip|*.tar.gz|*.tgz)
|
||||
tmpDir=$(mktemp -d) || {
|
||||
appError "Failed to create temporary database restore directory"
|
||||
return 1
|
||||
}
|
||||
|
||||
archiveExtract "$file" "$tmpDir" || {
|
||||
rm -rf -- "$tmpDir" &>/dev/null
|
||||
return 1
|
||||
}
|
||||
|
||||
local sqlCount
|
||||
sqlCount=$(find -- "$tmpDir" -type f -name "*.sql" | wc -l)
|
||||
if [[ "$sqlCount" -ne 1 ]]; then
|
||||
rm -rf -- "$tmpDir" &>/dev/null
|
||||
appError "Archive must contain exactly one SQL file: $file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
importFile=$(find -- "$tmpDir" -type f -name "*.sql" -print -quit)
|
||||
;;
|
||||
esac
|
||||
|
||||
local databaseName databaseUser databasePass
|
||||
databaseName=$(siteConfigGetOrSet "$domain" "databaseName" "$(mariadbDomain2id "$domain")")
|
||||
databaseUser=$(siteConfigGetOrSet "$domain" "databaseUser" "$(mariadbDomain2id "$domain")")
|
||||
databasePass=$(siteConfigGetOrCreate "$domain" "databasePass")
|
||||
|
||||
local dbExists=0
|
||||
if mariadbDatabaseExists "$databaseName"; then
|
||||
dbExists=1
|
||||
fi
|
||||
|
||||
local output rc
|
||||
|
||||
if (( dbExists == 0 )); then
|
||||
if ! mariadbDatabaseUserSet "$databaseName" "$databaseUser" "$databasePass"; then
|
||||
[[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null
|
||||
appError "Failed to create database/user"
|
||||
return 1
|
||||
fi
|
||||
|
||||
output=$(mariadbMasterImport "$databaseName" "$databaseUser" "$databasePass" "$importFile" 2>&1)
|
||||
rc=$?
|
||||
if [[ $rc -ne 0 ]]; then
|
||||
[[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null
|
||||
appError "mariadbMasterImport: $output"
|
||||
return 1
|
||||
fi
|
||||
|
||||
[[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null
|
||||
return 0
|
||||
fi
|
||||
|
||||
local ts tmpDb
|
||||
ts=$(date +%Y%m%d_%H%M%S)
|
||||
tmpDb="_test_${databaseName}_$ts"
|
||||
|
||||
if ! mariadbDatabaseUserSet "$tmpDb" "$databaseUser" "$databasePass"; then
|
||||
[[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null
|
||||
appError "Failed to prepare tmp database"
|
||||
return 1
|
||||
fi
|
||||
|
||||
output=$(mariadbMasterImport "$tmpDb" "$databaseUser" "$databasePass" "$importFile" 2>&1)
|
||||
rc=$?
|
||||
if [[ $rc -ne 0 ]]; then
|
||||
mariadbDatabaseRemove "$tmpDb"
|
||||
[[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null
|
||||
appError "Failed to import tmp database: $output"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! mariadbDatabaseRemove "$databaseName"; then
|
||||
mariadbDatabaseRemove "$tmpDb"
|
||||
[[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null
|
||||
appError "Failed to recreate target database: remove failed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! mariadbDatabaseUserSet "$databaseName" "$databaseUser" "$databasePass"; then
|
||||
mariadbDatabaseRemove "$tmpDb"
|
||||
[[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null
|
||||
appError "Failed to recreate target database: create failed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
output=$(mariadbMasterImport "$databaseName" "$databaseUser" "$databasePass" "$importFile" 2>&1)
|
||||
rc=$?
|
||||
if [[ $rc -ne 0 ]]; then
|
||||
mariadbDatabaseRemove "$tmpDb"
|
||||
[[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null
|
||||
appError "Failed to import database: $output"
|
||||
return 1
|
||||
fi
|
||||
|
||||
mariadbDatabaseRemove "$tmpDb"
|
||||
[[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Restores selected parts of a site from explicit source paths.
|
||||
#
|
||||
# Each source is optional. If a source path is provided, the corresponding part
|
||||
# of the site is restored. If a source path is empty, that part is skipped.
|
||||
#
|
||||
# $1 (domain): Site domain name.
|
||||
# $2 (sourceFiles): Optional source directory or archive file path with site files.
|
||||
# $3 (sourceDatabase): Optional source SQL file or archive file path with database dump.
|
||||
# $4 (sourceConf): Optional source OpenLiteSpeed virtual host config file path.
|
||||
#
|
||||
# Returns:
|
||||
# 0 on success, 1 on validation or restore failure.
|
||||
function restoreSite() {
|
||||
local domain error
|
||||
domain=$(domainPrepare "$1")
|
||||
if ! runError error domainCheck "$domain"; then
|
||||
printDanger "$siteLabel $error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local sourceFiles="$2"
|
||||
local sourceDatabase="$3"
|
||||
local sourceConf="$4"
|
||||
|
||||
# Files
|
||||
printInfo "$restoreLabel $domain | Files: $sourceFiles"
|
||||
if [[ -n "$sourceFiles" ]]; then
|
||||
if ! runError error restoreSiteFiles "$domain" "$sourceFiles"; then
|
||||
printDanger "$restoreLabel $domain | Files: $error"
|
||||
return 1
|
||||
fi
|
||||
printSuccess "$restoreLabel $domain | Files: Restoration complete"
|
||||
else
|
||||
printWarning "$restoreLabel $domain | Files: Skip restoring"
|
||||
fi
|
||||
|
||||
# Database
|
||||
printInfo "$restoreLabel $domain | Database: $sourceDatabase"
|
||||
if [[ -n "$sourceDatabase" ]]; then
|
||||
if ! runError error restoreSiteDatabase "$domain" "$sourceDatabase"; then
|
||||
printDanger "$restoreLabel $domain | Database: $error"
|
||||
return 1
|
||||
fi
|
||||
printSuccess "$restoreLabel $domain | Database: Restoration complete"
|
||||
else
|
||||
printWarning "$restoreLabel $domain | Database: Skip restoring"
|
||||
fi
|
||||
|
||||
# Config
|
||||
printInfo "$restoreLabel $domain | Config: $sourceConf"
|
||||
if [[ -n "$sourceConf" ]]; then
|
||||
if ! runError error cp -f -- "$sourceConf" "$olsVhostsConfigPath/$domain.conf"; then
|
||||
printDanger "$restoreLabel $domain | Config: $error"
|
||||
return 1
|
||||
fi
|
||||
printSuccess "$restoreLabel $domain | Config: Restoration complete"
|
||||
else
|
||||
printWarning "$restoreLabel $domain | Config: Skip restoring"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Restores a site from available backup entries.
|
||||
#
|
||||
# The function searches short-term and long-term backup paths for files,
|
||||
# database dumps, and OpenLiteSpeed virtual host configuration files matching
|
||||
# the specified domain. It can list available backups or restore a selected one.
|
||||
#
|
||||
# Supported index values:
|
||||
# empty: Show available backups and ask for a backup number.
|
||||
# list: Print available backup paths.
|
||||
# last: Restore the latest available backup.
|
||||
# full: Restore the latest backup that contains files, database, and config.
|
||||
# auto: Restore the latest full backup if available, otherwise the latest backup.
|
||||
# N: Restore backup by numeric index.
|
||||
#
|
||||
# $1 (domain): Site domain name.
|
||||
# $2 (index): Optional backup selector: list, last, full, auto, or numeric index.
|
||||
#
|
||||
# Returns:
|
||||
# 0 on success, 1 on validation, selection, or restore failure.
|
||||
function restoreRun() {
|
||||
local domain error
|
||||
domain=$(domainPrepare "$1")
|
||||
if ! runError error domainCheck "$domain"; then
|
||||
printDanger "$siteLabel $error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local index="$2"
|
||||
if [[ -n "$index" && ! "$index" =~ ^[0-9]+$ && "$index" != "auto" && "$index" != "last" && "$index" != "full" && "$index" != "list" ]]; then
|
||||
printDanger "Unknown value of index"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local backupEntryList=()
|
||||
local backupPathList=()
|
||||
local path
|
||||
|
||||
shopt -s nullglob
|
||||
local entryList=("$backupDailyPath"/*/*/"$domain"*)
|
||||
shopt -u nullglob
|
||||
for entry in "${entryList[@]}"; do
|
||||
backupEntryList+=("$entry")
|
||||
path=2:$(dirname -- "$entry")
|
||||
if ! arrayContains "$path" "${backupPathList[@]}"; then
|
||||
backupPathList+=("$path")
|
||||
fi
|
||||
done
|
||||
|
||||
shopt -s nullglob
|
||||
local entryList=("$backupArchivePath"/*/*/"$domain"*)
|
||||
shopt -u nullglob
|
||||
for entry in "${entryList[@]}"; do
|
||||
backupEntryList+=("$entry")
|
||||
path=1:$(dirname -- "$entry")
|
||||
if ! arrayContains "$path" "${backupPathList[@]}"; then
|
||||
backupPathList+=("$path")
|
||||
fi
|
||||
done
|
||||
|
||||
local backupSortList=($(printf "%s\n" "${backupPathList[@]}" | \
|
||||
sed "s/\/${backupFirstDir}$/\/./" | \
|
||||
sort -r | \
|
||||
sed "s/\/.$/\/${backupFirstDir}/"))
|
||||
|
||||
local bType bPath bTime bDate sourceList indexFull suffix
|
||||
local counter=1
|
||||
for marker in "${backupSortList[@]}"; do
|
||||
bType="${marker:0:1}"
|
||||
bPath="${marker:2}"
|
||||
bTime=$(basename "$bPath")
|
||||
bDate=$(basename -- $(dirname -- "$bPath"))
|
||||
|
||||
sourceList=()
|
||||
if arrayContains "$bPath/$domain" "${backupEntryList[@]}"; then
|
||||
sourceList+=('files:dir')
|
||||
fi
|
||||
if arrayContains "$bPath/$domain.tar.gz" "${backupEntryList[@]}"; then
|
||||
sourceList+=('files:tar')
|
||||
fi
|
||||
if arrayContains "$bPath/$domain.tgz" "${backupEntryList[@]}"; then
|
||||
sourceList+=('files:tar')
|
||||
fi
|
||||
if arrayContains "$bPath/$domain.zip" "${backupEntryList[@]}"; then
|
||||
sourceList+=('files:zip')
|
||||
fi
|
||||
if arrayContains "$bPath/$domain.sql" "${backupEntryList[@]}"; then
|
||||
sourceList+=('db:sql')
|
||||
fi
|
||||
if arrayContains "$bPath/$domain.sql.tar.gz" "${backupEntryList[@]}"; then
|
||||
sourceList+=('db:tar')
|
||||
fi
|
||||
if arrayContains "$bPath/$domain.sql.tgz" "${backupEntryList[@]}"; then
|
||||
sourceList+=('db:tar')
|
||||
fi
|
||||
if arrayContains "$bPath/$domain.sql.zip" "${backupEntryList[@]}"; then
|
||||
sourceList+=('db:zip')
|
||||
fi
|
||||
if arrayContains "$bPath/$domain.conf" "${backupEntryList[@]}"; then
|
||||
sourceList+=('conf')
|
||||
fi
|
||||
|
||||
if arrayContains "conf" "${sourceList[@]}" && \
|
||||
( arrayContains "db:sql" "${sourceList[@]}" || arrayContains "db:tar" "${sourceList[@]}" || arrayContains "db:zip" "${sourceList[@]}" ) && \
|
||||
( arrayContains "files:dir" "${sourceList[@]}" || arrayContains "files:tar" "${sourceList[@]}" || arrayContains "files:zip" "${sourceList[@]}" ); then
|
||||
suffix="\033[34m${sourceList[*]}\033[0m"
|
||||
if [[ -z "$indexFull" ]]; then
|
||||
indexFull="$counter"
|
||||
fi
|
||||
else
|
||||
suffix="${sourceList[*]}"
|
||||
fi
|
||||
if [[ "$bType" == "1" ]]; then
|
||||
suffix+=" | \033[33mLongTerm\033[0m"
|
||||
fi
|
||||
|
||||
if [[ -z "$index" ]]; then
|
||||
printf "%2s: %-19s | $suffix\n" "$counter" "$bDate/$bTime"
|
||||
fi
|
||||
((counter++))
|
||||
done
|
||||
|
||||
local indexSelect=
|
||||
case "$index" in
|
||||
list)
|
||||
for marker in "${backupSortList[@]}"; do
|
||||
printf "%s\n" "${marker:2}"
|
||||
done
|
||||
return 0
|
||||
;;
|
||||
last)
|
||||
indexSelect=1
|
||||
;;
|
||||
full)
|
||||
indexSelect="$indexFull"
|
||||
;;
|
||||
auto)
|
||||
if [[ -n "$indexFull" ]]; then
|
||||
indexSelect="$indexFull"
|
||||
else
|
||||
indexSelect=1
|
||||
fi
|
||||
;;
|
||||
[0-9]*)
|
||||
indexSelect="$index"
|
||||
;;
|
||||
*)
|
||||
printWarning "$domain" "Warning! The relevant data will be cleared before restoring."
|
||||
read -p "Specify the backup number: " indexSelect
|
||||
;;
|
||||
esac
|
||||
|
||||
((indexSelect--)) 2>/dev/null
|
||||
if [[ "$indexSelect" -lt 0 || "$indexSelect" -ge "${#backupSortList[@]}" ]]; then
|
||||
printDanger "Unknown index number"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local restorePath="${backupSortList[$indexSelect]:2}"
|
||||
if [[ ! -d "$restorePath" ]]; then
|
||||
printDanger "Path for restore not found"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local sourceFiles sourceDatabase sourceConf
|
||||
|
||||
if [[ -d "$restorePath/$domain" ]]; then
|
||||
sourceFiles="$restorePath/$domain"
|
||||
elif [[ -f "$restorePath/$domain.tar.gz" ]]; then
|
||||
sourceFiles="$restorePath/$domain.tar.gz"
|
||||
elif [[ -f "$restorePath/$domain.tgz" ]]; then
|
||||
sourceFiles="$restorePath/$domain.tgz"
|
||||
elif [[ -f "$restorePath/$domain.zip" ]]; then
|
||||
sourceFiles="$restorePath/$domain.zip"
|
||||
fi
|
||||
|
||||
if [[ -f "$restorePath/$domain.sql" ]]; then
|
||||
sourceDatabase="$restorePath/$domain.sql"
|
||||
elif [[ -f "$restorePath/$domain.sql.tar.gz" ]]; then
|
||||
sourceDatabase="$restorePath/$domain.sql.tar.gz"
|
||||
elif [[ -f "$restorePath/$domain.sql.tgz" ]]; then
|
||||
sourceDatabase="$restorePath/$domain.sql.tgz"
|
||||
elif [[ -f "$restorePath/$domain.sql.zip" ]]; then
|
||||
sourceDatabase="$restorePath/$domain.sql.zip"
|
||||
fi
|
||||
|
||||
if [[ -f "$restorePath/$domain.conf" ]]; then
|
||||
sourceConf="$restorePath/$domain.conf"
|
||||
fi
|
||||
|
||||
restoreSite "$domain" "$sourceFiles" "$sourceDatabase" "$sourceConf"
|
||||
|
||||
# Clean Redis
|
||||
redisDomainClean "$domain"
|
||||
|
||||
# Rebuild config site
|
||||
cmdSiteConfigRebuild "$domain"
|
||||
|
||||
# Rebuild config Wordpress
|
||||
wordpressConfigRebuild "$domain"
|
||||
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,729 @@
|
||||
# Prints a confirmation and returns 0 if confirmed.
|
||||
# $1 (query): query
|
||||
function cmdRouterConfirm() {
|
||||
local query="$1" confirmation
|
||||
|
||||
printRow
|
||||
read -r -p "${fontYellow}Warning!${fontReset} $query (y/n): " confirmation
|
||||
[[ "$confirmation" =~ ^[Yy]$ ]]
|
||||
}
|
||||
|
||||
function cmdK3s() {
|
||||
local action="${1:-}"
|
||||
shift || true
|
||||
|
||||
case "$action" in
|
||||
install)
|
||||
printTitle " $k3sLabel Install"
|
||||
if cmdRouterConfirm "Are you sure you want to$fontRed INSTALL$fontBlue ALL$fontReset resources to k3s?"; then
|
||||
cmdMariadbInstall
|
||||
cmdRedisInstall
|
||||
cmdOpenlitespeedInstall
|
||||
cmdPostfixInstall
|
||||
cmdWorkerInstall
|
||||
cmdMetricInstall
|
||||
fi
|
||||
;;
|
||||
uninstall)
|
||||
printTitle " $k3sLabel Uninstall"
|
||||
if cmdRouterConfirm "Are you sure you want to$fontRed UNINSTALL$fontBlue ALL$fontReset resources from k3s?"; then
|
||||
cmdK3sResourceClean
|
||||
fi
|
||||
;;
|
||||
info)
|
||||
printTitle " $k3sLabel Info"
|
||||
cmdK3sInfo
|
||||
;;
|
||||
status)
|
||||
printTitle " $k3sLabel Status"
|
||||
cmdK3sNodesStatus
|
||||
;;
|
||||
top)
|
||||
printTitle " $k3sLabel Top pod"
|
||||
cmdK3sTopPod
|
||||
;;
|
||||
res)
|
||||
printTitle " $k3sLabel Resources $@"
|
||||
cmdK3sResourceList "$@"
|
||||
;;
|
||||
*)
|
||||
printTitle " $k3sLabel"
|
||||
printRow
|
||||
printWarning "Unsupported or empty command: $action"
|
||||
cmdHelpK3S
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
function cmdMariadb() {
|
||||
local action="${1:-}"
|
||||
shift || true
|
||||
|
||||
case "$action" in
|
||||
install)
|
||||
printTitle " $mariadbLabel Install"
|
||||
if cmdRouterConfirm "Are you sure you want to$fontRed INSTALL$fontBlue MariaDB$fontReset resources to k3s?"; then
|
||||
cmdMariadbInstall
|
||||
fi
|
||||
;;
|
||||
update)
|
||||
printTitle " $mariadbLabel Update"
|
||||
if cmdRouterConfirm "Are you sure you want to$fontRed UPDATE$fontBlue MariaDB$fontReset resources in k3s?"; then
|
||||
cmdMariadbUpdate
|
||||
fi
|
||||
;;
|
||||
uninstall)
|
||||
printTitle " $mariadbLabel Uninstall"
|
||||
if cmdRouterConfirm "Are you sure you want to$fontRed UNINSTALL$fontBlue MariaDB$fontReset resources from k3s?"; then
|
||||
cmdMariadbUninstall
|
||||
fi
|
||||
;;
|
||||
info)
|
||||
printTitle " $mariadbLabel Info"
|
||||
cmdMariadbInfo
|
||||
;;
|
||||
status)
|
||||
printTitle " $mariadbLabel Status"
|
||||
cmdMariadbStatus
|
||||
;;
|
||||
replica)
|
||||
printTitle " $mariadbLabel Replica rebuild"
|
||||
if cmdRouterConfirm "Are you sure you want to$fontRed REBUILD$fontBlue MariaDB replica$fontReset?"; then
|
||||
cmdMariadbReplicaRebuild
|
||||
fi
|
||||
;;
|
||||
database)
|
||||
printTitle " $mariadbLabel Database list"
|
||||
cmdMariadbDatabase
|
||||
;;
|
||||
user)
|
||||
printTitle " $mariadbLabel User list"
|
||||
cmdMariadbUser
|
||||
;;
|
||||
dump)
|
||||
printTitle " $mariadbLabel Dump Master $@"
|
||||
cmdMariadbMasterDump "$@"
|
||||
;;
|
||||
dump_slave)
|
||||
printTitle " $mariadbLabel Dump Slave $@"
|
||||
cmdMariadbSlaveDump "$@"
|
||||
;;
|
||||
*)
|
||||
printTitle " $mariadbLabel"
|
||||
printRow
|
||||
printWarning "Unsupported or empty command: $action"
|
||||
cmdHelpMariaDB
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
function cmdRedis() {
|
||||
local action="${1:-}"
|
||||
shift || true
|
||||
|
||||
case "$action" in
|
||||
install)
|
||||
printTitle " $redisLabel Install"
|
||||
if cmdRouterConfirm "Are you sure you want to$fontRed INSTALL$fontBlue Redis$fontReset resources to k3s?"; then
|
||||
cmdRedisInstall
|
||||
fi
|
||||
;;
|
||||
update)
|
||||
printTitle " $redisLabel Update"
|
||||
if cmdRouterConfirm "Are you sure you want to$fontRed UPDATE$fontBlue Redis$fontReset resources in k3s?"; then
|
||||
cmdRedisUpdate
|
||||
fi
|
||||
;;
|
||||
uninstall)
|
||||
printTitle " $redisLabel Uninstall"
|
||||
if cmdRouterConfirm "Are you sure you want to$fontRed UNINSTALL$fontBlue Redis$fontReset resources from k3s?"; then
|
||||
cmdRedisUninstall
|
||||
fi
|
||||
;;
|
||||
info)
|
||||
printTitle " $redisLabel Info"
|
||||
cmdRedisInfo
|
||||
;;
|
||||
status)
|
||||
printTitle " $redisLabel Status"
|
||||
cmdRedisStatus
|
||||
;;
|
||||
user)
|
||||
printTitle " $redisLabel User list"
|
||||
cmdRedisUser
|
||||
;;
|
||||
flush)
|
||||
printTitle " $redisLabel Flush current DB"
|
||||
cmdRedisDatabaseFlush
|
||||
;;
|
||||
pass)
|
||||
printTitle " $redisLabel Update default (root) pass"
|
||||
cmdRedisRootPassUpdate
|
||||
;;
|
||||
*)
|
||||
printTitle " $redisLabel"
|
||||
printRow
|
||||
printWarning "Unsupported or empty command: $action"
|
||||
cmdHelpRedis
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
function cmdOpenlitespeed() {
|
||||
local action="${1:-}"
|
||||
shift || true
|
||||
|
||||
case "$action" in
|
||||
install)
|
||||
printTitle " $openlitespeedLabel Install"
|
||||
if cmdRouterConfirm "Are you sure you want to$fontRed INSTALL$fontBlue OpenLiteSpeed$fontReset resources to k3s?"; then
|
||||
cmdOpenlitespeedInstall
|
||||
fi
|
||||
;;
|
||||
update)
|
||||
printTitle " $openlitespeedLabel Update"
|
||||
if cmdRouterConfirm "Are you sure you want to$fontRed UPDATE$fontBlue OpenLiteSpeed$fontReset resources in k3s?"; then
|
||||
cmdOpenlitespeedUpdate
|
||||
fi
|
||||
;;
|
||||
uninstall)
|
||||
printTitle " $openlitespeedLabel Uninstall"
|
||||
if cmdRouterConfirm "Are you sure you want to$fontRed UNINSTALL$fontBlue OpenLiteSpeed$fontReset resources from k3s?"; then
|
||||
cmdOpenlitespeedUninstall
|
||||
fi
|
||||
;;
|
||||
info)
|
||||
printTitle " $openlitespeedLabel Info"
|
||||
cmdOpenlitespeedInfo
|
||||
;;
|
||||
list)
|
||||
printTitle " $openlitespeedLabel List (all|up|down)"
|
||||
cmdOpenlitespeedSiteList "$@"
|
||||
;;
|
||||
up)
|
||||
printTitle " $openlitespeedLabel Up $@"
|
||||
cmdOpenlitespeedVhostUp "$@"
|
||||
;;
|
||||
down)
|
||||
printTitle " $openlitespeedLabel Down $@"
|
||||
cmdOpenlitespeedVhostDown "$@"
|
||||
;;
|
||||
alias)
|
||||
printTitle " $openlitespeedLabel Alias $1"
|
||||
cmdOpenlitespeedVhostAliasSet "$@"
|
||||
;;
|
||||
restart)
|
||||
printTitle " $openlitespeedLabel Restart"
|
||||
cmdOpenlitespeedRestart
|
||||
;;
|
||||
php_kill)
|
||||
printTitle " $openlitespeedLabel Kill PHP $@"
|
||||
cmdOpenlitespeedPhpKill "$@"
|
||||
;;
|
||||
white_list)
|
||||
printTitle " $openlitespeedLabel White list update"
|
||||
cmdOpenlitespeedAdminWhiteList
|
||||
;;
|
||||
allow_list)
|
||||
printTitle " $openlitespeedLabel Allow list update"
|
||||
cmdOpenlitespeedAdminAllowList
|
||||
;;
|
||||
alias_list)
|
||||
printTitle " $openlitespeedLabel Alias list $1"
|
||||
cmdOpenlitespeedAliasList "$@"
|
||||
;;
|
||||
rebuild)
|
||||
printTitle " $openlitespeedLabel Rebuild $1"
|
||||
cmdOpenlitespeedVhostRebuild "$@"
|
||||
;;
|
||||
config)
|
||||
printTitle " $openlitespeedLabel Config check"
|
||||
cmdOpenlitespeedConfigCheck
|
||||
;;
|
||||
*)
|
||||
printTitle " $openlitespeedLabel"
|
||||
printRow
|
||||
printWarning "Unsupported or empty command: $action"
|
||||
cmdHelpOpenLiteSpeed
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
function cmdPostfix() {
|
||||
local action="${1:-}"
|
||||
shift || true
|
||||
|
||||
case "$action" in
|
||||
install)
|
||||
printTitle " $postfixLabel Install"
|
||||
if cmdRouterConfirm "Are you sure you want to$fontRed INSTALL$fontBlue Postfix$fontReset resources to k3s?"; then
|
||||
cmdPostfixInstall
|
||||
fi
|
||||
;;
|
||||
update)
|
||||
printTitle " $postfixLabel Update"
|
||||
if cmdRouterConfirm "Are you sure you want to$fontRed UPDATE$fontBlue Postfix$fontReset resources in k3s?"; then
|
||||
cmdPostfixUpdate
|
||||
fi
|
||||
;;
|
||||
uninstall)
|
||||
printTitle " $postfixLabel Uninstall"
|
||||
if cmdRouterConfirm "Are you sure you want to$fontRed UNINSTALL$fontBlue Postfix$fontReset resources from k3s?"; then
|
||||
cmdPostfixUninstall
|
||||
fi
|
||||
;;
|
||||
info)
|
||||
printTitle " $postfixLabel Info"
|
||||
cmdPostfixInfo
|
||||
;;
|
||||
status)
|
||||
printTitle " $postfixLabel Status"
|
||||
cmdPostfixMtaDnsCheck
|
||||
;;
|
||||
user)
|
||||
printTitle " $postfixLabel User list"
|
||||
cmdPostfixUser
|
||||
;;
|
||||
dkim)
|
||||
printInfo " $postfixLabel DKIM record for DNS (autocreate)"
|
||||
postfixDkimGet "$@"
|
||||
;;
|
||||
*)
|
||||
printTitle " $postfixLabel"
|
||||
printRow
|
||||
printWarning "Unsupported or empty command: $action"
|
||||
cmdHelpPostfix
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
function cmdWorker() {
|
||||
local action="${1:-}"
|
||||
shift || true
|
||||
|
||||
case "$action" in
|
||||
install)
|
||||
printTitle " $workerLabel Install"
|
||||
if cmdRouterConfirm "Are you sure you want to$fontRed INSTALL$fontBlue Worker$fontReset resources to k3s?"; then
|
||||
cmdWorkerInstall
|
||||
fi
|
||||
;;
|
||||
update)
|
||||
printTitle " $workerLabel Update"
|
||||
if cmdRouterConfirm "Are you sure you want to$fontRed UPDATE$fontBlue Worker$fontReset resources in k3s?"; then
|
||||
cmdWorkerUpdate
|
||||
fi
|
||||
;;
|
||||
uninstall)
|
||||
printTitle " $workerLabel Uninstall"
|
||||
if cmdRouterConfirm "Are you sure you want to$fontRed UNINSTALL$fontBlue Worker$fontReset resources from k3s?"; then
|
||||
cmdWorkerUninstall
|
||||
fi
|
||||
;;
|
||||
task)
|
||||
printInfo " $workerLabel Run $1"
|
||||
reportFile="$logPath/task/${appDate}_$appTime.log"
|
||||
printText "Start: $appDate $appTime\n"
|
||||
cmdWorkerTaskHandle "$@"
|
||||
printText "\nFinish: $(date +%Y-%m-%d) $(date +%H-%M-%S) | Time: $(( $(date +%s) - scriptStart ))s"
|
||||
reportFile=""
|
||||
;;
|
||||
list)
|
||||
printTitle " $workerLabel Task list"
|
||||
cmdWorkerTaskList
|
||||
;;
|
||||
down)
|
||||
printInfo " $workerLabel Put on pause..."
|
||||
workerAgentStop
|
||||
;;
|
||||
up)
|
||||
printInfo " $workerLabel Resuming from pause..."
|
||||
workerAgentStart
|
||||
;;
|
||||
*)
|
||||
printTitle " $workerLabel"
|
||||
printRow
|
||||
printWarning "Unsupported or empty command: $action"
|
||||
cmdHelpWorker
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
function cmdMetric() {
|
||||
local action="${1:-}"
|
||||
shift || true
|
||||
|
||||
case "$action" in
|
||||
install)
|
||||
printTitle " $metricLabel Install"
|
||||
if cmdRouterConfirm "Are you sure you want to$fontRed INSTALL$fontBlue Metric$fontReset resources to k3s?"; then
|
||||
cmdMetricInstall
|
||||
fi
|
||||
;;
|
||||
update)
|
||||
printTitle " $metricLabel Update"
|
||||
if cmdRouterConfirm "Are you sure you want to$fontRed UPDATE$fontBlue Metric$fontReset resources in k3s?"; then
|
||||
cmdMetricUpdate
|
||||
fi
|
||||
;;
|
||||
uninstall)
|
||||
printTitle " $metricLabel Uninstall"
|
||||
if cmdRouterConfirm "Are you sure you want to$fontRed UNINSTALL$fontBlue Metric$fontReset resources from k3s?"; then
|
||||
cmdMetricUninstall
|
||||
fi
|
||||
;;
|
||||
restart)
|
||||
printInfo " $metricLabel Restart"
|
||||
metricRestart
|
||||
;;
|
||||
*)
|
||||
printTitle " $metricLabel"
|
||||
printRow
|
||||
printWarning "Unsupported or empty command: $action"
|
||||
cmdHelpMetric
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
function cmdSite() {
|
||||
local action="${1:-}"
|
||||
shift || true
|
||||
|
||||
case "$action" in
|
||||
add)
|
||||
printTitle " $siteLabel Add $*"
|
||||
cmdSiteAdd "$@"
|
||||
;;
|
||||
add_wp|wp)
|
||||
printTitle " $siteLabel Add Wordpress $*"
|
||||
cmdSiteAddWordpress "$@"
|
||||
;;
|
||||
remove)
|
||||
printTitle " $siteLabel Remove $*"
|
||||
cmdSiteRemove "$@"
|
||||
;;
|
||||
copy)
|
||||
printTitle " $siteLabel Copy $*"
|
||||
cmdSiteCopy "$@"
|
||||
;;
|
||||
rename)
|
||||
printTitle " $siteLabel Rename $*"
|
||||
cmdSiteRename "$@"
|
||||
;;
|
||||
rebuild)
|
||||
printTitle " $siteLabel Rebuild config $*"
|
||||
cmdSiteConfigRebuild "$@"
|
||||
;;
|
||||
rebuild_wp)
|
||||
printTitle " $siteLabel Wordpress config rebuild $*"
|
||||
cmdSiteWordpressRebuild "$@"
|
||||
;;
|
||||
reset_pass)
|
||||
printTitle " $siteLabel Reset ALL passwords for vhost $*"
|
||||
cmdSitePasswordReset "$@"
|
||||
;;
|
||||
reset_auth)
|
||||
printTitle " $siteLabel Reset ALL auth settings for vhost $*"
|
||||
cmdSiteAuthReset "$@"
|
||||
;;
|
||||
dump)
|
||||
printTitle " $siteLabel Database dump $*"
|
||||
cmdSiteDatabaseDump "$@"
|
||||
;;
|
||||
info)
|
||||
printTitle " $siteLabel Info $*"
|
||||
cmdSiteInfo "$@"
|
||||
;;
|
||||
status)
|
||||
printTitle " $siteLabel Status $*"
|
||||
cmdSiteStatus "$@"
|
||||
;;
|
||||
*)
|
||||
printTitle " $siteLabel"
|
||||
printRow
|
||||
printWarning "Unsupported or empty command: $action"
|
||||
cmdHelpSite
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
function cmdWordpress() {
|
||||
local action="${1:-}"
|
||||
shift || true
|
||||
|
||||
case "$action" in
|
||||
user)
|
||||
printTitle " $wordpressLabel User list"
|
||||
cmdWordpressUserList "$@"
|
||||
;;
|
||||
pass)
|
||||
printTitle " $wordpressLabel User password set"
|
||||
cmdWordpressPasswordSet "$@"
|
||||
;;
|
||||
salt)
|
||||
printTitle " $wordpressLabel Shuffle salts $*"
|
||||
cmdWordpressSalt "$@"
|
||||
;;
|
||||
check)
|
||||
printTitle " $wordpressLabel Check core and plugins $*"
|
||||
cmdWordpressCheck "$@"
|
||||
;;
|
||||
exec)
|
||||
printTitle " $wordpressLabel Command exec $*"
|
||||
cmdWordpressExec "$@"
|
||||
;;
|
||||
*)
|
||||
printTitle " $wordpressLabel"
|
||||
printRow
|
||||
printWarning "Unsupported or empty command: $action"
|
||||
cmdHelpWP
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
function cmdSftp() {
|
||||
local action="${1:-}"
|
||||
shift || true
|
||||
|
||||
case "$action" in
|
||||
user)
|
||||
printTitle " $systemLabel SFTP User list $*"
|
||||
cmdSftpUserList "$@"
|
||||
;;
|
||||
enable)
|
||||
printTitle " $systemLabel SFTP Access enable $*"
|
||||
cmdSftpAccessEnable "$@"
|
||||
;;
|
||||
disable)
|
||||
printTitle " $systemLabel SFTP Access disable $*"
|
||||
cmdSftpAccessDisable "$@"
|
||||
;;
|
||||
reset_pass)
|
||||
printTitle " $systemLabel SFTP Reset pass"
|
||||
cmdSftpPasswordSet "$@"
|
||||
;;
|
||||
support)
|
||||
printTitle " $systemLabel SFTP Adding support"
|
||||
if cmdRouterConfirm "Changes will be made to the$fontRed SSH configuration$fontReset. Make sure there is a$fontBlue backup way to connect$fontReset to the server. Continue?"; then
|
||||
cmdSftpAddingSupport
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
printTitle " $systemLabel SFTP"
|
||||
printRow
|
||||
printWarning "Unsupported or empty command: $action"
|
||||
cmdHelpSftp
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
function cmdCron() {
|
||||
local action="${1:-}"
|
||||
shift || true
|
||||
|
||||
case "$action" in
|
||||
add)
|
||||
printTitle " $systemLabel Cron job add"
|
||||
cmdCronAdd
|
||||
;;
|
||||
remove)
|
||||
printTitle " $systemLabel Cron job remove"
|
||||
cmdCronRemove
|
||||
;;
|
||||
list)
|
||||
printTitle " $systemLabel Cron job list"
|
||||
cmdCronList
|
||||
;;
|
||||
*)
|
||||
printTitle " $systemLabel Cron"
|
||||
printRow
|
||||
printWarning "Unsupported or empty command: $action"
|
||||
cmdHelpCron
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
function cmdBackup() {
|
||||
local action="${1:-}"
|
||||
shift || true
|
||||
|
||||
case "$action" in
|
||||
run)
|
||||
printTitle " $systemLabel Backup of target"
|
||||
cmdBackupRun "$@"
|
||||
;;
|
||||
list)
|
||||
printTitle " $systemLabel List of backups on local storage"
|
||||
cmdBackupList
|
||||
;;
|
||||
size)
|
||||
printTitle " $systemLabel List of Backup Sizes"
|
||||
cmdBackupSize
|
||||
;;
|
||||
# remove)
|
||||
# printTitle " $backupLabel Remove of backups on local storage"
|
||||
# cmdStorageBackupRemove
|
||||
# ;;
|
||||
*)
|
||||
printTitle " $systemLabel Backup"
|
||||
printRow
|
||||
printWarning "Unsupported or empty command: $action"
|
||||
cmdHelpBackup
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
function cmdRestore() {
|
||||
local action="${1:-}"
|
||||
shift || true
|
||||
|
||||
case "$action" in
|
||||
run)
|
||||
printTitle " $restoreLabel Backup of target"
|
||||
restoreRun "$@"
|
||||
;;
|
||||
*)
|
||||
printTitle " $restoreLabel"
|
||||
printRow
|
||||
printWarning "Unsupported or empty command: $action"
|
||||
cmdHelpRestore
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
function cmdStorage() {
|
||||
local action="${1:-}"
|
||||
shift || true
|
||||
|
||||
case "$action" in
|
||||
list)
|
||||
printTitle " $storageLabel List of backups on external storage"
|
||||
cmdStorageBackupList
|
||||
;;
|
||||
compare)
|
||||
printTitle " $systemLabel Comparison table"
|
||||
cmdStorageBackupCompare
|
||||
;;
|
||||
size)
|
||||
printTitle " $systemLabel List of Backup Sizes on external storage"
|
||||
cmdStorageBackupSize
|
||||
;;
|
||||
sync)
|
||||
printTitle " $systemLabel Synchronization with external storage"
|
||||
cmdStorageBackupSync "$@"
|
||||
;;
|
||||
remove)
|
||||
printTitle " $systemLabel Remove of backups on external storage"
|
||||
cmdStorageBackupRemove
|
||||
;;
|
||||
*)
|
||||
printTitle " $systemLabel Storage"
|
||||
printRow
|
||||
printWarning "Unsupported or empty command: $action"
|
||||
cmdHelpStorage
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Dispatches a named script sub-command and redirects output to a timestamped log file.
|
||||
# $1 (option): script name (backup, mariadb-dump, worker-observer, metric-kube, metric-sites, diag-report-ai-short, diag-report-ai-full).
|
||||
function cmdScript() {
|
||||
cmdScriptRun "$@"
|
||||
}
|
||||
|
||||
function cmdInstall() {
|
||||
printTitle " $ks3Label Full install"
|
||||
if cmdRouterConfirm "Are you sure you want to$fontRed INSTALL$fontReset $fontBlue FULL infrastrutute$fontReset?"; then
|
||||
cmdInstallFull
|
||||
fi
|
||||
}
|
||||
|
||||
function cmdTest() {
|
||||
local action="${1:-}"
|
||||
shift || true
|
||||
|
||||
case "$action" in
|
||||
mariadb)
|
||||
printInfo " $testLabel MariaDB replica"
|
||||
testMariadbReplica
|
||||
;;
|
||||
redis)
|
||||
printInfo " $testLabel Redis cluster"
|
||||
testRedisCluster
|
||||
;;
|
||||
site)
|
||||
printInfo " $testLabel Site"
|
||||
testSite
|
||||
;;
|
||||
wordpress)
|
||||
printInfo " $testLabel Wordpress"
|
||||
testSiteWordpress
|
||||
;;
|
||||
*)
|
||||
printTitle " $testLabel SFTP"
|
||||
printRow
|
||||
printWarning "Unsupported or empty command: $action"
|
||||
cmdHelpTest
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
function cmdMalware() {
|
||||
local action="${1:-}"
|
||||
shift || true
|
||||
|
||||
case "$action" in
|
||||
check)
|
||||
printTitle " Malware check"
|
||||
cmdScriptMalwareCheck
|
||||
;;
|
||||
remove)
|
||||
printTitle " Malware remove"
|
||||
cmdScriptMalwareRemove
|
||||
;;
|
||||
*)
|
||||
printTitle " $testLabel Malware"
|
||||
printRow
|
||||
printWarning "Unsupported or empty command: $action"
|
||||
cmdHelpMalware
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
function cmdRouter() {
|
||||
local action="${1:-}"
|
||||
shift || true
|
||||
|
||||
printHeader
|
||||
|
||||
case "$action" in
|
||||
-k|--k3s) cmdK3s "$@" ;;
|
||||
-m|--mariadb) cmdMariadb "$@" ;;
|
||||
-r|--redis) cmdRedis "$@" ;;
|
||||
-o|--ols) cmdOpenlitespeed "$@" ;;
|
||||
-p|--postfix) cmdPostfix "$@" ;;
|
||||
-w|--worker) cmdWorker "$@" ;;
|
||||
-s|--site) cmdSite "$@" ;;
|
||||
--metric) cmdMetric "$@" ;;
|
||||
--wp) cmdWordpress "$@" ;;
|
||||
--sftp) cmdSftp "$@" ;;
|
||||
--cron) cmdCron "$@" ;;
|
||||
--shell) cmdShell "$@" ;;
|
||||
--log) cmdLog "$@" ;;
|
||||
--describe) cmdDescribe "$@" ;;
|
||||
--delete) cmdDelete "$@" ;;
|
||||
--backup) cmdBackup "$@" ;;
|
||||
--restore) cmdRestore "$@" ;;
|
||||
--storage) cmdStorage "$@" ;;
|
||||
--script) cmdScript "$@" ;;
|
||||
--install) cmdInstall "$@" ;;
|
||||
--test) cmdTest "$@" ;;
|
||||
--malware) cmdMalware "$@" ;;
|
||||
--help) cmdHelp "$@" ;;
|
||||
dev) appDev "$@" ;;
|
||||
esac
|
||||
|
||||
local status=$?
|
||||
printFooter
|
||||
return "$status"
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
scriptLabel="[Script]"
|
||||
|
||||
# Runs the site backup dispatch with script logging.
|
||||
function cmdScriptBackup() {
|
||||
printDotText "Script" "Backup sites"
|
||||
printStart
|
||||
cmdBackupDispatch
|
||||
printFinish
|
||||
}
|
||||
|
||||
# Runs the MariaDB master full-dump script with script logging.
|
||||
function cmdScriptMariadbDump() {
|
||||
printDotText "Script" "MariaDB database dump"
|
||||
printStart
|
||||
cmdMariadbMasterDump
|
||||
printFinish
|
||||
}
|
||||
|
||||
# Runs the worker task observer with script logging.
|
||||
function cmdScriptWorkerObserver() {
|
||||
printDotText "Script" "Worker observer"
|
||||
printStart
|
||||
cmdWorkerObserver
|
||||
printFinish
|
||||
}
|
||||
|
||||
# Collects and pushes kube and lsphp metrics with script logging.
|
||||
function cmdScriptMetricKube() {
|
||||
printDotText "Script" "Metric KUBE"
|
||||
printStart
|
||||
metricKube
|
||||
metricLsphp
|
||||
printFinish
|
||||
}
|
||||
|
||||
# Collects and pushes per-site metrics with script logging.
|
||||
function cmdScriptMetricSites() {
|
||||
printDotText "Script" "Metric sites"
|
||||
printStart
|
||||
metricSites
|
||||
printFinish
|
||||
}
|
||||
|
||||
# Runs the AI diagnostic report with script logging.
|
||||
# [$1] (mode): report mode: short (default) or full.
|
||||
function cmdScriptDiagReportAi() {
|
||||
local mode
|
||||
mode=${1:-short}
|
||||
|
||||
printDotText "Script" "Diag report AI $mode"
|
||||
printStart
|
||||
diagRun "$mode"
|
||||
printFinish
|
||||
}
|
||||
|
||||
function cmdScriptMalwareCheck() {
|
||||
printDotText "Script" "Malware check"
|
||||
printStart
|
||||
|
||||
olsVhostsPath="$vhostsPath"
|
||||
|
||||
cmdMalwareFilesDelete list
|
||||
cmdMalwareUserDelete list
|
||||
cmdMalwareOptionsDelete list
|
||||
cmdMalwareCronDelete list
|
||||
|
||||
printFinish
|
||||
}
|
||||
|
||||
function cmdScriptMalwareRemove() {
|
||||
printDotText "Script" "Malware remove"
|
||||
printStart
|
||||
|
||||
olsVhostsPath="$vhostsPath"
|
||||
|
||||
cmdMalwareFilesDelete remove
|
||||
cmdMalwareUserDelete remove
|
||||
cmdMalwareOptionsDelete remove
|
||||
cmdMalwareCronDelete remove
|
||||
|
||||
printFinish
|
||||
}
|
||||
|
||||
function cmdScriptRun() {
|
||||
local option="$1"
|
||||
|
||||
printText "Output: $logPath/$option/${appDate}_$appTime.log"
|
||||
printRow
|
||||
|
||||
printFile="$logPath/$option/${appDate}_$appTime.log"
|
||||
local logFileOld="$logFile"
|
||||
logFile=""
|
||||
|
||||
case "$option" in
|
||||
backup)
|
||||
cmdScriptBackup
|
||||
;;
|
||||
mariadb-dump)
|
||||
cmdScriptMariadbDump
|
||||
;;
|
||||
worker-observer)
|
||||
cmdScriptWorkerObserver
|
||||
;;
|
||||
metric-kube)
|
||||
cmdScriptMetricKube
|
||||
;;
|
||||
metric-sites)
|
||||
cmdScriptMetricSites
|
||||
;;
|
||||
diag-report-ai-short)
|
||||
cmdScriptDiagReportAi "short"
|
||||
;;
|
||||
diag-report-ai-full)
|
||||
cmdScriptDiagReportAi "full"
|
||||
;;
|
||||
malware-remove)
|
||||
cmdScriptMalwareRemove
|
||||
;;
|
||||
unigma)
|
||||
cmdUnigma
|
||||
;;
|
||||
*)
|
||||
printFile=""
|
||||
logFile="$logFileOld"
|
||||
|
||||
printTitle " $scriptLabel"
|
||||
printRow
|
||||
printWarning "Unsupported or empty command: $action"
|
||||
cmdHelpScript
|
||||
;;
|
||||
esac
|
||||
|
||||
printFile=""
|
||||
logFile="$logFileOld"
|
||||
}
|
||||
@@ -0,0 +1,837 @@
|
||||
siteLabel="[Site]"
|
||||
siteWordpressLabel="[Wordpress]"
|
||||
|
||||
# Creates a new site: validates domain/source/DB, creates OLS vhost, copies files,
|
||||
# sets up MariaDB/Redis/Postfix, rolls back via cmdSiteRemove on failure.
|
||||
# $1 (domain): site domain name.
|
||||
# $2 (sourceFiles): source directory or archive with site files.
|
||||
# [$3] (sourceDatabase): optional SQL dump or archive with SQL dump.
|
||||
function cmdSiteAdd() {
|
||||
local domain sourceFiles
|
||||
domain=$(domainPrepare "$1")
|
||||
shift || true
|
||||
|
||||
sourceFiles="${1:-$appAssetsPath/vhost/default}"
|
||||
shift || true
|
||||
|
||||
printSection "Add site"
|
||||
printDotText "domain" "$domain"
|
||||
|
||||
if ! runError error domainCheck "$domain"; then
|
||||
printDotText "status" "$labelFail"
|
||||
printDanger "$error"
|
||||
elif ! run output error siteAdd "$domain" "$sourceFiles" "$@"; then
|
||||
printDotText "status" "$labelFail"
|
||||
printDanger "$error"
|
||||
else
|
||||
printDotText "status" "$labelDone"
|
||||
|
||||
cmdOpenlitespeedRestart
|
||||
|
||||
fi
|
||||
}
|
||||
|
||||
# Removes a site and all associated MariaDB/Redis/Postfix/OLS/host resources.
|
||||
# $1 (domain): site domain name.
|
||||
function cmdSiteRemove() {
|
||||
local domain mode
|
||||
domain=$(domainPrepare "$1")
|
||||
shift || true
|
||||
|
||||
mode="$1"
|
||||
shift || true
|
||||
|
||||
if [[ ! "$mode" == "-f" && ! "$mode" == "--force" ]]; then
|
||||
if ! cmdRouterConfirm "Are you sure you want to$fontRed DELETE$fontReset the site $fontBlue$domain$fontReset?"; then
|
||||
printRow
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
printSection "Remove site"
|
||||
printDotText "domain" "$domain"
|
||||
|
||||
if ! runError error domainCheck "$domain"; then
|
||||
printDotText "status" "$labelFail"
|
||||
printDanger "$error"
|
||||
elif ! run output error siteRemove "$domain"; then
|
||||
printDotText "status" "$labelFail"
|
||||
printDanger "$error"
|
||||
else
|
||||
printDotText "status" "$labelDone"
|
||||
|
||||
cmdOpenlitespeedRestart
|
||||
fi
|
||||
}
|
||||
|
||||
# Copies a site: exports source DB, creates target site, rebuilds WP config.
|
||||
# $1 (domain): source site domain name.
|
||||
# $2 (domainNew): target site domain name.
|
||||
function cmdSiteCopy() {
|
||||
local domain domainNew
|
||||
domain=$(domainPrepare "$1")
|
||||
domainNew=$(domainPrepare "$2")
|
||||
|
||||
printSection "Copy site"
|
||||
printDotText "source" "$domain"
|
||||
printDotText "target" "$domainNew"
|
||||
|
||||
if ! runError error domainCheck "$domain"; then
|
||||
printDotText "status" "$labelFail"
|
||||
printDanger "$error"
|
||||
elif ! runError error domainCheck "$domainNew"; then
|
||||
printDotText "status" "$labelFail"
|
||||
printDanger "$error"
|
||||
elif ! run output error siteCopy "$domain" "$domainNew"; then
|
||||
printDotText "status" "$labelFail"
|
||||
printDanger "$error"
|
||||
else
|
||||
printDotText "status" "$labelDone"
|
||||
|
||||
cmdOpenlitespeedRestart
|
||||
|
||||
if ! runError error wordpressDomainUpdate "$domainNew"; then
|
||||
printDotText "update" "$labelFail"
|
||||
printDanger "$error"
|
||||
else
|
||||
printDotText "update" "$labelDone"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Renames a site by copying it to the new domain and removing the old one.
|
||||
# $1 (domain): current site domain name.
|
||||
# $2 (domainNew): new site domain name.
|
||||
function cmdSiteRename() {
|
||||
local domain domainNew
|
||||
domain=$(domainPrepare "$1")
|
||||
domainNew=$(domainPrepare "$2")
|
||||
|
||||
printSection "Rename site"
|
||||
printDotText "source" "$domain"
|
||||
printDotText "target" "$domainNew"
|
||||
|
||||
if ! runError error domainCheck "$domain"; then
|
||||
printDotText "status" "$labelFail"
|
||||
printDanger "$error"
|
||||
elif ! runError error domainCheck "$domainNew"; then
|
||||
printDotText "status" "$labelFail"
|
||||
printDanger "$error"
|
||||
elif ! run output error siteRename "$domain" "$domainNew"; then
|
||||
printDotText "status" "$labelFail"
|
||||
printDanger "$error"
|
||||
else
|
||||
printDotText "status" "$labelDone"
|
||||
|
||||
cmdOpenlitespeedRestart
|
||||
|
||||
if ! runError error wordpressDomainUpdate "$domainNew"; then
|
||||
printDotText "update" "$labelFail"
|
||||
printDanger "$error"
|
||||
else
|
||||
printDotText "update" "$labelDone"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Creates a WordPress site from the bundled template, then rebuilds WP config.
|
||||
# $1 (domain): site domain name.
|
||||
# [$2] (sourceFiles): optional source directory or archive.
|
||||
# [$@] (...): optional remaining arguments passed to cmdSiteAdd (e.g. database dump).
|
||||
function cmdSiteAddWordpress() {
|
||||
local domain sourceFiles
|
||||
domain=$(domainPrepare "$1")
|
||||
shift || true
|
||||
|
||||
sourceFiles="${1:-$appAssetsPath/vhost/wordpress}"
|
||||
if [[ ! -e "$sourceFiles" ]]; then
|
||||
sourceFiles="$appAssetsPath/vhost/wordpress.tar.gz"
|
||||
fi
|
||||
shift || true
|
||||
|
||||
printSection "Add site (Wordpress)"
|
||||
printDotText "domain" "$domain"
|
||||
|
||||
if ! runError error domainCheck "$domain"; then
|
||||
printDotText "status" "$labelFail"
|
||||
printDanger "$error"
|
||||
elif ! run output error siteAdd "$domain" "$sourceFiles" "$@"; then
|
||||
printDotText "status" "$labelFail"
|
||||
printDanger "$error"
|
||||
else
|
||||
printDotText "status" "$labelDone"
|
||||
|
||||
cmdOpenlitespeedRestart
|
||||
|
||||
if ! run output error wordpressConfigRebuild "$domain"; then
|
||||
printDanger "$error"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Rebuilds OLS vhost/config, MariaDB, Redis, Postfix, and WordPress salt for a site.
|
||||
# $1 (domain): site domain name.
|
||||
function cmdSiteConfigRebuild() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
printRow
|
||||
|
||||
if ! runError error openlitespeedVhostRebuild "$domain"; then
|
||||
printDotText "OLS vhost" "$labelFail"
|
||||
printDanger "$error"
|
||||
else
|
||||
printDotText "OLS vhost" "$labelDone"
|
||||
fi
|
||||
|
||||
if ! runError error openlitespeedConfigVhostSet "$domain"; then
|
||||
printDotText "OLS config" "$labelFail"
|
||||
printDanger "$error"
|
||||
else
|
||||
printDotText "OLS config" "$labelDone"
|
||||
fi
|
||||
|
||||
if ! runError error mariadbConfigRebuild "$domain"; then
|
||||
printDotText "MariaDB" "$labelFail"
|
||||
printDanger "$error"
|
||||
else
|
||||
printDotText "MariaDB" "$labelDone"
|
||||
fi
|
||||
|
||||
if ! runError error redisConfigRebuild "$domain"; then
|
||||
printDotText "Redis" "$labelFail"
|
||||
printDanger "$error"
|
||||
else
|
||||
printDotText "Redis" "$labelDone"
|
||||
fi
|
||||
|
||||
if ! runError error postfixConfigRebuild "$domain"; then
|
||||
printDotText "Postfix" "$labelFail"
|
||||
printDanger "$error"
|
||||
else
|
||||
printDotText "Postfix" "$labelDone"
|
||||
fi
|
||||
|
||||
if ! runError error wordpressExec "$domain" config shuffle-salts; then
|
||||
printDotText "Wordpress salt" "$labelFail"
|
||||
printDanger "$error"
|
||||
else
|
||||
printDotText "Wordpress salt" "$labelDone"
|
||||
fi
|
||||
}
|
||||
|
||||
# Rebuilds the WordPress wp-config.php for a site.
|
||||
# $1 (domain): site domain name.
|
||||
function cmdSiteWordpressRebuild() {
|
||||
local error
|
||||
|
||||
printRow
|
||||
|
||||
if ! runError error wordpressConfigRebuild "$@"; then
|
||||
printDotText "Wordpress config rebuild" "$labelFail"
|
||||
printDanger "$error"
|
||||
else
|
||||
printDotText "Wordpress config rebuild" "$labelDone"
|
||||
fi
|
||||
}
|
||||
|
||||
# Resets databasePass/redisPass/postfixPass and rebuilds configs for one site or all sites.
|
||||
# Skips sites without wp-config.php in "all" mode.
|
||||
# $1 (target): site domain name or "all".
|
||||
function cmdSitePasswordReset() {
|
||||
local target="$1"
|
||||
local vhostList error
|
||||
|
||||
printRow
|
||||
|
||||
if [[ -z "$target" ]]; then
|
||||
printDanger "Target not specified";
|
||||
elif ! run vhostList error openlitespeedConfigVhostList; then
|
||||
printDanger "Cannot get vhost list: $error";
|
||||
elif [[ "$target" == "all" ]]; then
|
||||
local domain
|
||||
for domain in $vhostList; do
|
||||
if ! runError error sitePasswordReset "$domain"; then
|
||||
printDotText "$domain" "$labelFail"
|
||||
printDanger "$error"
|
||||
else
|
||||
printDotText "$domain" "$labelDone"
|
||||
fi
|
||||
done
|
||||
elif ! listContains "$target" "$vhostList"; then
|
||||
printDanger "Unknown domain: $target";
|
||||
elif ! runError error sitePasswordReset "$target"; then
|
||||
printDotText "$target" "$labelFail"
|
||||
printDanger "$error"
|
||||
else
|
||||
printDotText "$target" "$labelDone"
|
||||
fi
|
||||
}
|
||||
|
||||
# Resets all service credentials (passwords + usernames) and rebuilds configs for one site or all sites.
|
||||
# Skips sites without wp-config.php in "all" mode.
|
||||
# $1 (target): site domain name or "all".
|
||||
function cmdSiteAuthReset() {
|
||||
local target="$1"
|
||||
local vhostList error
|
||||
|
||||
printRow
|
||||
|
||||
if [[ -z "$target" ]]; then
|
||||
printDanger "Target not specified";
|
||||
elif ! run vhostList error openlitespeedConfigVhostList; then
|
||||
printDanger "Cannot get vhost list: $error";
|
||||
elif [[ "$target" == "all" ]]; then
|
||||
local domain
|
||||
for domain in $vhostList; do
|
||||
if ! runError error siteAuthReset "$domain"; then
|
||||
printDotText "$domain" "$labelFail"
|
||||
printDanger "$error"
|
||||
else
|
||||
printDotText "$domain" "$labelDone"
|
||||
fi
|
||||
done
|
||||
elif ! listContains "$target" "$vhostList"; then
|
||||
printDanger "Unknown domain: $target";
|
||||
elif ! runError error siteAuthReset "$target"; then
|
||||
printDotText "$target" "$labelFail"
|
||||
printDanger "$error"
|
||||
else
|
||||
printDotText "$target" "$labelDone"
|
||||
fi
|
||||
}
|
||||
|
||||
# Exports a site's database to a file.
|
||||
# $1 (domain): site domain name.
|
||||
# [$2] (file): target dump file (auto-generated if omitted).
|
||||
function cmdSiteDatabaseDump() {
|
||||
local domain error
|
||||
domain=$(domainPrepare "$1")
|
||||
if ! runError error domainCheck "$domain"; then
|
||||
printDanger "$siteLabel $error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local file="$2"
|
||||
if [[ -z "$file" ]]; then
|
||||
file="$appDataPath/mariadb/${appDate}_${appTime}_$domain.sql.tar.gz"
|
||||
fi
|
||||
|
||||
local databaseName databaseUser databasePass
|
||||
databaseName=$(siteConfigGet "$domain" "databaseName")
|
||||
databaseUser=$(siteConfigGet "$domain" "databaseUser")
|
||||
databasePass=$(siteConfigGet "$domain" "databasePass")
|
||||
|
||||
printRow
|
||||
printDotText "file" "$file"
|
||||
printDotText "base" "$databaseName"
|
||||
printDotText "user" "$databaseUser"
|
||||
|
||||
if ! runError error mariadbMasterExport "$databaseUser" "$databasePass" "$databaseName" "$file"; then
|
||||
printDotText "status" "$labelFailed"
|
||||
printDanger "$error"
|
||||
else
|
||||
printDotText "status" "$labelDone"
|
||||
fi
|
||||
}
|
||||
|
||||
# Prints system, config, and OLS details for a site.
|
||||
# $1 (domain): site domain name.
|
||||
function cmdSiteInfo() {
|
||||
printRow
|
||||
|
||||
local domain error
|
||||
domain=$(domainPrepare "$1")
|
||||
if ! runError error domainCheck "$domain"; then
|
||||
printDanger "$error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
printSection "System"
|
||||
local ug userId groupId
|
||||
ug=$(domainToUser "$domain")
|
||||
printDotText "user" "$ug"
|
||||
printDotText "group" "$ug"
|
||||
if ! run userId error id -u "$ug"; then
|
||||
printDotText "userID" "$labelUnknown"
|
||||
else
|
||||
printDotText "userID" "$fontGreen$userId$fontReset"
|
||||
fi
|
||||
if ! run groupId error id -g "$ug"; then
|
||||
printDotText "groupID" "$labelUnknown"
|
||||
else
|
||||
printDotText "groupID" "$fontGreen$groupId$fontReset"
|
||||
fi
|
||||
|
||||
local ip
|
||||
ip=$(systemHostGet "$domain")
|
||||
if [[ -z "$ip" ]]; then
|
||||
printDotText "/etc/hosts" "${fontGray}null$fontReset"
|
||||
elif [[ "$ip" == '127.0.0.1' ]]; then
|
||||
printDotText "/etc/hosts" "$fontGreen$ip$fontReset"
|
||||
else
|
||||
printDotText "/etc/hosts" "$fontRed$ip$fontReset"
|
||||
fi
|
||||
|
||||
local limits blockLimit inodeLimit
|
||||
if ! run limits error openlitespeedVhostQuotaGet "$ug"; then
|
||||
printDotText "quota block limit" "$labelUnknown"
|
||||
printDotText "quota inode limit" "$labelUnknown"
|
||||
else
|
||||
read -r blockLimit inodeLimit <<< "$limits"
|
||||
blockLimit=$(numFormat "$blockLimit"000)
|
||||
inodeLimit=$(numFormat "$inodeLimit")
|
||||
printDotText "quota block limit" "$blockLimit"
|
||||
printDotText "quota inode limit" "$inodeLimit"
|
||||
fi
|
||||
|
||||
if groups "$ug" | grep -qw "$sftpAccessGroup"; then
|
||||
printDotText "SFTP access" "$labelOn"
|
||||
else
|
||||
printDotText "SFTP access" "$labelOff"
|
||||
fi
|
||||
|
||||
printSection "Config"
|
||||
# printDotText "file$fontReset" "$appDataPath/config/$domain.config"
|
||||
if [[ ! -f "$appDataPath/config/$domain.config" ]]; then
|
||||
printDotText "file" "$fontRed$appDataPath/config/$domain.config$fontReset"
|
||||
printDotText "file" "${fontRed}not found$fontReset"
|
||||
else
|
||||
printDotText "file" "$fontGreen$appDataPath/config/$domain.config$fontReset"
|
||||
|
||||
local -a params
|
||||
local param value
|
||||
params=(alias databaseName databaseUser databasePass redisUser redisPass postfixUser postfixPass postfixForwardTo sftpPass quotaBlockLimit quotaInodeLimit)
|
||||
for param in "${params[@]}"; do
|
||||
value=$(siteConfigGet "$domain" "$param")
|
||||
printDotText "$param" "${value:-$labelNull}"
|
||||
done
|
||||
fi
|
||||
|
||||
printSection "OpenLiteSpeed"
|
||||
if [[ ! -f "$olsVhostsConfigPath/$domain.conf" ]]; then
|
||||
printDotText "file$fontReset" "$fontRed$olsVhostsConfigPath/$domain.conf$fontReset"
|
||||
printDotText "file$fontReset" "${fontRed}not found$fontReset"
|
||||
else
|
||||
printDotText "file$fontReset" "$fontGreen$olsVhostsConfigPath/$domain.conf$fontReset"
|
||||
fi
|
||||
|
||||
if [[ ! -d "$olsVhostsPath/$domain" ]]; then
|
||||
printDotText "path$fontReset" "$fontRed$olsVhostsPath/$domain$fontReset"
|
||||
printDotText "path$fontReset" "${fontRed}not found$fontReset"
|
||||
else
|
||||
printDotText "path$fontReset" "$fontGreen$olsVhostsPath/$domain$fontReset"
|
||||
fi
|
||||
|
||||
if [[ ! -d "$olsPrivatePath/$domain" ]]; then
|
||||
printDotText "data$fontReset" "$fontRed$olsPrivatePath/$domain$fontReset"
|
||||
printDotText "data$fontReset" "${fontRed}not found$fontReset"
|
||||
else
|
||||
printDotText "data$fontReset" "$fontGreen$olsPrivatePath/$domain$fontReset"
|
||||
fi
|
||||
}
|
||||
|
||||
# Checks site resources (config, system, dirs, OLS, MariaDB, Redis, Postfix, HTTP).
|
||||
# $1 (domain): site domain name.
|
||||
# [$@] (opts): full|short (mode).
|
||||
function cmdSiteStatus() {
|
||||
local domain opt error section label note
|
||||
domain=$(domainPrepare "$1")
|
||||
if ! runError error domainCheck "$domain"; then
|
||||
printDanger "$siteLabel $error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
mode="${2:-full}"
|
||||
|
||||
printSection "Config"
|
||||
if [[ -f "$appDataPath/config/$domain.config" ]]; then
|
||||
printDotText "file" "$fontGreen$appDataPath/config/$domain.config$fontReset"
|
||||
|
||||
local -a params
|
||||
local param value
|
||||
params=(databaseName databaseUser databasePass redisUser redisPass postfixUser postfixPass quotaBlockLimit quotaInodeLimit)
|
||||
for param in "${params[@]}"; do
|
||||
value=$(siteConfigGet "$domain" "$param")
|
||||
if [[ -n "$value" ]]; then
|
||||
printDotText "$param" "$labelPass"
|
||||
else
|
||||
printDotText "$param" "$labelFail"
|
||||
fi
|
||||
done
|
||||
else
|
||||
printDotText "file" "$fontRed$appDataPath/config/$domain.config$fontReset"
|
||||
fi
|
||||
|
||||
printSection "System"
|
||||
local userId groupId ug
|
||||
ug=$(domainToUser "$domain")
|
||||
note="$fontGray$ug$fontReset"
|
||||
if ! run userId error id -u "$ug"; then
|
||||
printDotText "user" "$note $labelFail"
|
||||
else
|
||||
printDotText "user" "$note $labelPass"
|
||||
fi
|
||||
if ! run groupId error id -g "$ug"; then
|
||||
printDotText "group" "$note $labelFail"
|
||||
else
|
||||
printDotText "group" "$note $labelPass"
|
||||
fi
|
||||
|
||||
local ip
|
||||
ip=$(systemHostGet "$domain")
|
||||
note="$fontGray$ip$fontReset"
|
||||
if [[ "$ip" != '127.0.0.1' ]]; then
|
||||
printDotText "/etc/hosts" "$note $labelFail"
|
||||
else
|
||||
printDotText "/etc/hosts" "$note $labelPass"
|
||||
fi
|
||||
|
||||
local limits blockLimit inodeLimit
|
||||
if ! run limits error openlitespeedVhostQuotaGet "$ug"; then
|
||||
printDotText "quota block limit" "$labelFail"
|
||||
printDotText "quota inode limit" "$labelFail"
|
||||
else
|
||||
read -r blockLimit inodeLimit <<< "$limits"
|
||||
blockLimit=$(numFormat "$blockLimit"000)
|
||||
inodeLimit=$(numFormat "$inodeLimit")
|
||||
printDotText "quota block limit" "$blockLimit $labelPass"
|
||||
printDotText "quota inode limit" "$inodeLimit $labelPass"
|
||||
fi
|
||||
|
||||
local sftpConfig
|
||||
if ! sftpConfig=$(sshd -T -C user="$ug",host="$hostName",addr=127.0.0.1); then
|
||||
printDotText "SFTP config" "$labelFail"
|
||||
else
|
||||
label="$fontBlue SFTP ForceCommand$fontReset"
|
||||
if ! grep -Fxq "forcecommand internal-sftp -d /www -u 027" <<< "$sftpConfig"; then
|
||||
printDotText "SFTP ForceCommand" "$labelFail"
|
||||
else
|
||||
printDotText "SFTP ForceCommand" "$labelPass"
|
||||
fi
|
||||
if ! grep -Fxq "chrootdirectory %h" <<< "$sftpConfig"; then
|
||||
printDotText "SFTP ChrootDirectory" "$labelFail"
|
||||
else
|
||||
printDotText "SFTP ChrootDirectory" "$labelPass"
|
||||
fi
|
||||
fi
|
||||
|
||||
printSection "Path | existence, owner, permissions, ACL:nobody"
|
||||
local path
|
||||
path="$olsVhostsPath/$domain"
|
||||
label="vhost $fontBlue/$fontReset"
|
||||
note="${fontGray}755:root:root$fontReset"
|
||||
if [[ ! -d "$path" ]]; then
|
||||
printDotText "$label" "$note $labelFail"
|
||||
elif ! fileSignatureCheck "$path" "755:root:root"; then
|
||||
printDotText "$label" "$note $labelFail"
|
||||
else
|
||||
printDotText "$label" "$note $labelPass"
|
||||
fi
|
||||
|
||||
path="$olsVhostsPath/$domain/www"
|
||||
label="vhost $fontBlue/www$fontReset"
|
||||
note="${fontGray}2750:u:g acl:r-x$fontReset"
|
||||
if [[ ! -d "$path" ]]; then
|
||||
printDotText "$label" "$note $labelFail"
|
||||
elif ! fileSignatureCheck "$path" "2750:$ug:$ug"; then
|
||||
printDotText "$label" "$note $labelFail"
|
||||
elif ! fileSignatureAclCheck "$path" "user:nobody:r-x"; then
|
||||
printDotText "$label" "$note $labelFail"
|
||||
else
|
||||
printDotText "$label" "$note $labelPass"
|
||||
fi
|
||||
|
||||
local -a dirs
|
||||
local dir
|
||||
dirs=(session tmp)
|
||||
for dir in "${dirs[@]}"; do
|
||||
path="$olsVhostsPath/$domain/$dir"
|
||||
label="vhost $fontBlue/$dir$fontReset"
|
||||
note="${fontGray}770:u:g acl:rwx$fontReset"
|
||||
if [[ ! -d "$path" ]]; then
|
||||
printDotText "$label" "$note $labelFail"
|
||||
elif ! fileSignatureCheck "$path" "770:$ug:$ug"; then
|
||||
printDotText "$label" "$note $labelFail"
|
||||
elif ! fileSignatureAclCheck "$path" "user:nobody:rwx"; then
|
||||
printDotText "$label" "$note $labelFail"
|
||||
else
|
||||
printDotText "$label" "$note $labelPass"
|
||||
fi
|
||||
done
|
||||
|
||||
path="$olsPrivatePath/$domain"
|
||||
label="vhost-data $fontBlue/$fontReset"
|
||||
note="${fontGray}750:u:g acl:r-x$fontReset"
|
||||
if [[ ! -d "$path" ]]; then
|
||||
printDotText "$label" "$note $labelFail"
|
||||
elif ! fileSignatureCheck "$path" "750:$ug:$ug"; then
|
||||
printDotText "$label" "$note $labelFail"
|
||||
elif ! fileSignatureAclCheck "$path" "user:nobody:r-x"; then
|
||||
printDotText "$label" "$note $labelFail"
|
||||
else
|
||||
printDotText "$label" "$note $labelPass"
|
||||
fi
|
||||
|
||||
path="$olsPrivatePath/$domain/bootstrap.php"
|
||||
label="vhost-data $fontBlue/bootstrap.php$fontReset"
|
||||
note="${fontGray}600:u:g acl:r--$fontReset"
|
||||
if [[ ! -f "$path" ]]; then
|
||||
printDotText "$label" "$note $labelFail"
|
||||
elif ! fileSignatureCheck "$path" "600:$ug:$ug"; then
|
||||
printDotText "$label" "$note $labelFail"
|
||||
elif ! fileSignatureAclCheck "$path" "user:nobody:r--"; then
|
||||
printDotText "$label" "$note $labelFail"
|
||||
else
|
||||
printDotText "$label" "$note $labelPass"
|
||||
fi
|
||||
# fileSignatureAclDiff "$path" "user:nobody:r--"
|
||||
|
||||
printSection "OpenLiteSpeed"
|
||||
if ! run vhostList error openlitespeedConfigVhostList; then
|
||||
printDotText "get vhost list" "$labelFail"
|
||||
else
|
||||
note="$fontGray$domain$fontReset"
|
||||
if listContains "$domain" "$vhostList"; then
|
||||
printDotText "vhost" "$note $labelPass"
|
||||
else
|
||||
printDotText "vhost" "$note $labelFail"
|
||||
fi
|
||||
fi
|
||||
note="$fontGray$domain.conf$fontReset"
|
||||
if [[ ! -f "$olsVhostsConfigPath/$domain.conf" ]]; then
|
||||
printDotText "config" "$note $labelFail"
|
||||
else
|
||||
printDotText "config" "$note $labelPass"
|
||||
fi
|
||||
|
||||
printSection "MariaDB"
|
||||
local mariadbDatabaseList mariadbUserList
|
||||
if ! run mariadbDatabaseList error mariadbDatabaseListGet; then
|
||||
printDotText "get database list" "$labelFail"
|
||||
elif ! run mariadbUserList error mariadbUserListGet; then
|
||||
printDotText "get user list" "$labelFail"
|
||||
else
|
||||
mariadbDatabase=$(siteConfigGet "$domain" "databaseName")
|
||||
note="$fontGray$mariadbDatabase$fontReset"
|
||||
if ! listContains "$mariadbDatabase" "$mariadbDatabaseList"; then
|
||||
printDotText "base" "$note $labelFail"
|
||||
else
|
||||
printDotText "base" "$note $labelPass"
|
||||
fi
|
||||
|
||||
mariadbUser=$(siteConfigGet "$domain" "databaseUser")
|
||||
note="$fontGray$mariadbUser$fontReset"
|
||||
if ! listContains "$mariadbUser" "$mariadbUserList"; then
|
||||
printDotText "user" "$note $labelFail"
|
||||
else
|
||||
printDotText "user" "$note $labelPass"
|
||||
fi
|
||||
|
||||
if [[ "$mode" == "full" ]]; then
|
||||
mariadbPass=$(siteConfigGet "$domain" "databasePass")
|
||||
if ! run output error mariadbMasterExec mariadb -u"$mariadbUser" -p"$mariadbPass" -N -e "SELECT 1;"; then
|
||||
printDotText "access" "$labelFail"
|
||||
elif [[ "$output" != "1" ]]; then
|
||||
printDotText "access" "$labelFail"
|
||||
else
|
||||
printDotText "access" "$labelPass"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
printSection "Redis"
|
||||
local redisUserList
|
||||
if ! run redisUserList error redisUserListGet; then
|
||||
printDotText "$fontBlue get user list$fontReset" "$labelFail"
|
||||
else
|
||||
redisUser=$(siteConfigGet "$domain" "redisUser")
|
||||
note="$fontGray$redisUser$fontReset"
|
||||
if ! listContains "$redisUser" "$redisUserList"; then
|
||||
printDotText "user" "$note $labelFail"
|
||||
else
|
||||
printDotText "user" "$note $labelPass"
|
||||
fi
|
||||
if [[ "$mode" == "full" ]]; then
|
||||
redisPass=$(siteConfigGet "$domain" "redisPass")
|
||||
if ! run output error redisExec redis-cli --no-auth-warning --user "$redisUser" --pass "$redisPass" PING; then
|
||||
printDotText "access" "$labelFail"
|
||||
elif [[ "$output" != "PONG" ]]; then
|
||||
printDotText "access" "$labelFail"
|
||||
else
|
||||
printDotText "access" "$labelPass"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
printSection "Postfix"
|
||||
local postfixSaslUserList
|
||||
if ! run postfixSaslUserList error postfixSaslUserList; then
|
||||
printDotText "get SASL list" "$labelFail"
|
||||
else
|
||||
postfixUser=$(siteConfigGet "$domain" "postfixUser")
|
||||
note="$fontGray$postfixUser$fontReset"
|
||||
if ! listContains "$postfixUser@$postfixDefaultRealm" "$postfixSaslUserList"; then
|
||||
printDotText "SASL user" "$note $labelFail"
|
||||
else
|
||||
printDotText "SASL user" "$note $labelPass"
|
||||
fi
|
||||
postfixPass=$(siteConfigGet "$domain" "postfixPass")
|
||||
printDotText "SASL access" "${fontGray}in progress$fontReset"
|
||||
fi
|
||||
|
||||
if [[ "$mode" == "full" ]]; then
|
||||
printSection "HTTP"
|
||||
local httpCode urlEffective
|
||||
if ! run httpCode error urlCode "http://$domain"; then
|
||||
printDotText "HTTP code" "${fontGray}unknown$fontReset"
|
||||
elif [[ "$httpCode" == 200 ]]; then
|
||||
printDotText "HTTP code" "$fontGreen$httpCode$fontReset"
|
||||
elif [[ "$httpCode" =~ ^[23][0-9]{2}$ ]]; then
|
||||
printDotText "HTTP code" "$fontYellow$httpCode$fontReset"
|
||||
else
|
||||
printDotText "HTTP code" "$fontRed$httpCode$fontReset"
|
||||
fi
|
||||
if ! run urlEffective error urlAccessible "$domain"; then
|
||||
printDotText "URL effective" "${fontGray}unknown$fontReset"
|
||||
else
|
||||
printDotText "URL effective" "$fontGreen$urlEffective$fontReset"
|
||||
fi
|
||||
|
||||
printSection "Files"
|
||||
siteFilesCheck "$domain"
|
||||
fi
|
||||
}
|
||||
|
||||
function siteFilesCheck() {
|
||||
local domain error ug dots
|
||||
domain=$(domainPrepare "$1")
|
||||
if ! runError error domainCheck "$domain"; then
|
||||
printDanger "$error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
ug=$(domainToUser "$domain")
|
||||
dots=30
|
||||
|
||||
# fileSignatureDiff "$olsVhostsPath/$domain" "755:root:root"
|
||||
if ! run output error fileSignatureDiff "$olsVhostsPath/$domain" "755:root:root"; then
|
||||
printDanger "${error:-$output}"
|
||||
elif [[ -n "$output" ]]; then
|
||||
local prefix=":$olsVhostsPath/$domain"
|
||||
printDot "$dots" "${fontCyan}vhost d 755:root:root$fontReset" "" "${output/$prefix/ /}"
|
||||
fi
|
||||
|
||||
# fileSignatureDiffList "$olsVhostsPath/$domain/www" "d" "2750|$ug:$ug"
|
||||
if ! run output error fileSignatureDiffList "$olsVhostsPath/$domain/www" "d" "2750|$ug:$ug"; then
|
||||
printDanger "${error:-$output}"
|
||||
else
|
||||
lineCount=$(grep -c . <<< "$output" || true)
|
||||
if [[ "$lineCount" -gt 0 ]]; then
|
||||
local label="${fontCyan}www d 2750:u:g$fontReset"
|
||||
local prefix=":$olsVhostsPath/$domain/www/"
|
||||
while read -r line; do
|
||||
printDot "$dots" "$label" "" "${line/$prefix/ /}"
|
||||
done < <(awk 'NF' <<< "$output")
|
||||
fi
|
||||
fi
|
||||
|
||||
# fileSignatureDiffList "$olsVhostsPath/$domain/www" "f" "(600|640):$ug:$ug"
|
||||
if ! run output error fileSignatureDiffList "$olsVhostsPath/$domain/www" "f" "(600|640):$ug:$ug"; then
|
||||
printDanger "${error:-$output}"
|
||||
else
|
||||
lineCount=$(grep -c . <<< "$output" || true)
|
||||
if [[ "$lineCount" -gt 0 ]]; then
|
||||
local label="${fontCyan}www f (600|640):u:g$fontReset"
|
||||
local prefix=":$olsVhostsPath/$domain/www/"
|
||||
while read -r line; do
|
||||
printDot "$dots" "$label" "" "${line/$prefix/ /}"
|
||||
done < <(awk 'NF' <<< "$output")
|
||||
fi
|
||||
fi
|
||||
|
||||
# fileSignatureDiffList "$olsVhostsPath/$domain/tmp" "d" "770:$ug:$ug"
|
||||
if ! run output error fileSignatureDiffList "$olsVhostsPath/$domain/tmp" "d" "770:$ug:$ug"; then
|
||||
printDanger "${error:-$output}"
|
||||
else
|
||||
lineCount=$(grep -c . <<< "$output" || true)
|
||||
if [[ "$lineCount" -gt 0 ]]; then
|
||||
local label="${fontCyan}tmp d 770:u:g$fontReset"
|
||||
local prefix=":$olsVhostsPath/$domain/tmp/"
|
||||
while read -r line; do
|
||||
printDot "$dots" "$label" "" "${line/$prefix/ /}"
|
||||
done < <(awk 'NF' <<< "$output")
|
||||
fi
|
||||
fi
|
||||
|
||||
# fileSignatureDiffList "$olsVhostsPath/$domain/tmp" "f" "660:$ug:$ug"
|
||||
if ! run output error fileSignatureDiffList "$olsVhostsPath/$domain/tmp" "f" "660:$ug:$ug"; then
|
||||
printDanger "${error:-$output}"
|
||||
else
|
||||
lineCount=$(grep -c . <<< "$output" || true)
|
||||
if [[ "$lineCount" -gt 0 ]]; then
|
||||
local label="${fontCyan}tmp f 660:u:g$fontReset"
|
||||
local prefix=":$olsVhostsPath/$domain/tmp/"
|
||||
while read -r line; do
|
||||
printDot "$dots" "$label" "" "${line/$prefix/ /}"
|
||||
done < <(awk 'NF' <<< "$output")
|
||||
fi
|
||||
fi
|
||||
|
||||
# fileSignatureDiffList "$olsVhostsPath/$domain/session" "d" "770:$ug:$ug"
|
||||
if ! run output error fileSignatureDiffList "$olsVhostsPath/$domain/session" "d" "770:$ug:$ug"; then
|
||||
printDanger "${error:-$output}"
|
||||
else
|
||||
lineCount=$(grep -c . <<< "$output" || true)
|
||||
if [[ "$lineCount" -gt 0 ]]; then
|
||||
local label="${fontCyan}session d 770:u:g$fontReset"
|
||||
local prefix=":$olsVhostsPath/$domain/session/"
|
||||
while read -r line; do
|
||||
printDot "$dots" "$label" "" "${line/$prefix/ /}"
|
||||
done < <(awk 'NF' <<< "$output")
|
||||
fi
|
||||
fi
|
||||
|
||||
# fileSignatureDiffList "$olsVhostsPath/$domain/session" "f" "(660|600):$ug:$ug"
|
||||
if ! run output error fileSignatureDiffList "$olsVhostsPath/$domain/session" "f" "(660|600):$ug:$ug"; then
|
||||
printDanger "${error:-$output}"
|
||||
else
|
||||
lineCount=$(grep -c . <<< "$output" || true)
|
||||
if [[ "$lineCount" -gt 0 ]]; then
|
||||
local label="${fontCyan}session f (660|600):u:g$fontReset"
|
||||
local prefix=":$olsVhostsPath/$domain/session/"
|
||||
while read -r line; do
|
||||
printDot "$dots" "$label" "" "${line/$prefix/ /}"
|
||||
done < <(awk 'NF' <<< "$output")
|
||||
fi
|
||||
fi
|
||||
|
||||
# fileSignatureDiffList "$olsPrivatePath/$domain" "d" "750:$ug:$ug"
|
||||
if ! run output error fileSignatureDiffList "$olsPrivatePath/$domain" "d" "750:$ug:$ug"; then
|
||||
printDanger "${error:-$output}"
|
||||
else
|
||||
lineCount=$(grep -c . <<< "$output" || true)
|
||||
if [[ "$lineCount" -gt 0 ]]; then
|
||||
local label="${fontCyan}data d 750:u:g$fontReset"
|
||||
local prefix=":$olsPrivatePath/$domain/"
|
||||
while read -r line; do
|
||||
printDot "$dots" "$label" "" "${line/$prefix/ /}"
|
||||
done < <(awk 'NF' <<< "$output")
|
||||
fi
|
||||
fi
|
||||
|
||||
# fileSignatureDiffList "$olsPrivatePath/$domain" "f" "600:$ug:$ug"
|
||||
if ! run output error fileSignatureDiffList "$olsPrivatePath/$domain" "f" "600:$ug:$ug"; then
|
||||
printDanger "${error:-$output}"
|
||||
else
|
||||
lineCount=$(grep -c . <<< "$output" || true)
|
||||
if [[ "$lineCount" -gt 0 ]]; then
|
||||
local label="${fontCyan}data f 600:u:g$fontReset"
|
||||
local prefix=":$olsPrivatePath/$domain/"
|
||||
while read -r line; do
|
||||
printDot "$dots" "$label" "" "${line/$prefix/ /}"
|
||||
done < <(awk 'NF' <<< "$output")
|
||||
fi
|
||||
fi
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
storageLabel="[Storage]"
|
||||
|
||||
# Lists all backup directories on external storage with type labels (archive, daily, or unknown).
|
||||
function cmdStorageBackupList() {
|
||||
local dirList error dirCount archiveList dailyList
|
||||
run dirList error storageFileList "/" d || { printDanger "$error"; return 1; }
|
||||
dirCount=$(grep -c . <<< "$dirList" || true)
|
||||
archiveList=$(printf '%s\n' "$dirList" | grep -E -- "$backupArchiveDirPattern" | sort || true)
|
||||
dailyList=$(printf '%s\n' "$dirList" | grep -E -- "$backupDailyDirPattern" | sort || true)
|
||||
|
||||
printRow
|
||||
|
||||
local i=0 num dir label
|
||||
while read -r dir; do
|
||||
((++i))
|
||||
num=$(printf "%0${#dirCount}d" "$i")
|
||||
label="$num: $fontBlue$dir$fontReset"
|
||||
|
||||
if listContains "$dir" "$archiveList"; then
|
||||
printDotText "$label" "${fontGreen}archive$fontReset"
|
||||
elif listContains "$dir" "$dailyList"; then
|
||||
printDotText "$label" "${fontGreen}daily$fontReset"
|
||||
else
|
||||
printDotText "$label" "$labelUnknown"
|
||||
fi
|
||||
done < <(awk 'NF' <<< "$dirList")
|
||||
}
|
||||
|
||||
# Rotates archive backups on external storage: deletes old entries exceeding $storageArchiveKeep.
|
||||
function cmdStorageBackupArchiveDispatch() {
|
||||
local dirList error
|
||||
run dirList error storageFileList "/" d || { printDanger "$error"; return 1; }
|
||||
|
||||
printSection "Config"
|
||||
printDotText "Type" "$storageType"
|
||||
printDotText "Path" "$rsyncPath"
|
||||
printDotText "Archive keep" "$storageArchiveKeep"
|
||||
printDotText "Archive pattern" "$backupArchiveDirPattern"
|
||||
|
||||
printSection "Directories found"
|
||||
archiveList=$(printf '%s\n' "$dirList" | grep -E -- "$backupArchiveDirPattern" | sort || true)
|
||||
local archiveCount archiveRemoveCount i=0 num label dirDate dirDays archiveRemoveList=""
|
||||
archiveCount=$(grep -c . <<< "$archiveList" || true)
|
||||
if [[ "$archiveCount" -gt 0 ]]; then
|
||||
while read -r dir; do
|
||||
((++i))
|
||||
num=$(printf "%0${#archiveCount}d" "$i")
|
||||
label="$num: $fontBlue$dir$fontReset"
|
||||
|
||||
if (( archiveCount - storageArchiveKeep >= i )); then
|
||||
printDotText "$label" "${fontRed}delete$fontReset"
|
||||
archiveRemoveList+=$'\n'"$dir"
|
||||
else
|
||||
printDotText "$label" "${fontGreen}save$fontReset"
|
||||
fi
|
||||
done < <(awk 'NF' <<< "$archiveList")
|
||||
|
||||
archiveRemoveCount=$(grep -c . <<< "$archiveRemoveList" || true)
|
||||
if [[ "$archiveRemoveCount" -gt 0 ]]; then
|
||||
printSection "Deleting Directories"
|
||||
while read -r dir; do
|
||||
label="$dir"
|
||||
if ! runError error storageBackupRemove "$dir"; then
|
||||
printDotText "$label" "$labelFail"
|
||||
printDanger "$error"
|
||||
else
|
||||
printDotText "$label" "$labelDone"
|
||||
fi
|
||||
done < <(awk 'NF' <<< "$archiveRemoveList")
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Rotates daily backups on external storage: deletes old entries exceeding $storageDailyKeep, skips today.
|
||||
function cmdStorageBackupDailyDispatch() {
|
||||
local dirList error
|
||||
run dirList error storageFileList "/" d || { printDanger "$error"; return 1; }
|
||||
|
||||
printSection "Config"
|
||||
printDotText "Type" "$storageType"
|
||||
printDotText "Path" "$rsyncPath"
|
||||
printDotText "Daily keep" "$storageDailyKeep"
|
||||
printDotText "Daily pattern" "$backupDailyDirPattern"
|
||||
|
||||
printSection "Directories found"
|
||||
dailyList=$(printf '%s\n' "$dirList" | grep -v "$appDate" | grep -E -- "$backupDailyDirPattern" | sort || true)
|
||||
local dailyCount dailyRemoveCount i=0 num label dailyRemoveList=""
|
||||
dailyCount=$(grep -c . <<< "$dailyList" || true)
|
||||
if [[ "$dailyCount" -gt 0 ]]; then
|
||||
while read -r dir; do
|
||||
((++i))
|
||||
num=$(printf "%0${#dailyCount}d" "$i")
|
||||
label="$num: $fontBlue$dir$fontReset"
|
||||
|
||||
if [[ "$dir" == "$appDate" ]]; then
|
||||
printDotText "$label" "${fontGray}skipped$fontReset"
|
||||
elif (( dailyCount - storageDailyKeep >= i )); then
|
||||
printDotText "$label" "${fontRed}delete$fontReset"
|
||||
dailyRemoveList+=$'\n'"$dir"
|
||||
else
|
||||
printDotText "$label" "${fontGreen}save$fontReset"
|
||||
fi
|
||||
done < <(awk 'NF' <<< "$dailyList")
|
||||
|
||||
dailyRemoveCount=$(grep -c . <<< "$dailyRemoveList" || true)
|
||||
if [[ "$dailyRemoveCount" -gt 0 ]]; then
|
||||
printSection "Deleting Directories"
|
||||
while read -r dir; do
|
||||
label="$dir"
|
||||
if ! runError error storageBackupRemove "$dir"; then
|
||||
printDotText "$label" "$labelFail"
|
||||
printDanger "$error"
|
||||
else
|
||||
printDotText "$label" "$labelDone"
|
||||
fi
|
||||
done < <(awk 'NF' <<< "$dailyRemoveList")
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Synchronizes the local path to external storage (rsync or SSH).
|
||||
# $1 (sourcePath): local path to sync.
|
||||
# [$2] (type): storage type (rsync or ssh); defaults to $storageType.
|
||||
function cmdStoragePathSync() {
|
||||
local sourcePath="$1"
|
||||
[[ -n "$sourcePath" ]] || { printDanger "Source path not specified"; return 1; }
|
||||
|
||||
local type="${2:-$storageType}"
|
||||
local error
|
||||
|
||||
printSection "Config"
|
||||
printDotText "Source" "$sourcePath"
|
||||
if [[ ! -e "$sourcePath" ]]; then
|
||||
printDotText "$sourcePath" "${fontRed}not found$fontReset"
|
||||
return 1
|
||||
fi
|
||||
|
||||
case "$type" in
|
||||
rsync)
|
||||
printDotText "Type" "$type"
|
||||
printDotText "rSync URI" "$rsyncUri"
|
||||
printDotText "rSync path" "$rsyncPath"
|
||||
printSection "Executing"
|
||||
runError error storagePathSyncRsync "$sourcePath" || {
|
||||
printDotText "synchronization" "$labelFail"
|
||||
printDanger "$error"
|
||||
return 1
|
||||
}
|
||||
;;
|
||||
ssh)
|
||||
printDotText "Type" "$type"
|
||||
printDotText "SSH URI" "$sshUser@$sshHost"
|
||||
printDotText "SSH path" "$sshPath"
|
||||
printSection "Executing"
|
||||
runError error storagePathSyncSSH "$sourcePath" || {
|
||||
printDotText "synchronization" "$labelFail"
|
||||
printDanger "$error"
|
||||
return 1
|
||||
}
|
||||
;;
|
||||
*)
|
||||
printDotText "Type" "$type $labelUnknown"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
printDotText "synchronization" "$labelDone"
|
||||
}
|
||||
|
||||
# Syncs today's daily backup ($backupDailyPath/$appDate) to external storage.
|
||||
function cmdStorageBackupDailySyncLast() {
|
||||
cmdStoragePathSync "$backupDailyPath/$appDate"
|
||||
}
|
||||
|
||||
# Syncs the newest archive backup directory to external storage.
|
||||
function cmdStorageBackupArchiveSyncLast() {
|
||||
local dirList archiveList archiveDir error
|
||||
run dirList error fileList "$backupArchivePath" d || { printDanger "$error"; return 1; }
|
||||
archiveList=$(printf '%s\n' "$dirList" | grep -E -- "$backupArchiveDirPattern" | sort || true)
|
||||
archiveDir=$(tail -n 1 <<< "$archiveList")
|
||||
[[ -n "$archiveDir" ]] || { printDanger "No archive directories found"; return 1; }
|
||||
|
||||
cmdStoragePathSync "$backupArchivePath/$archiveDir"
|
||||
}
|
||||
|
||||
# Interactively selects a local archive backup directory and syncs it to external storage.
|
||||
function cmdStorageBackupArchiveSync() {
|
||||
local dirList error archiveList archiveCount
|
||||
run dirList error fileList "$backupArchivePath" d || { printDanger "$error"; return 1; }
|
||||
archiveList=$(printf '%s\n' "$dirList" | grep -E -- "$backupArchiveDirPattern" | sort || true)
|
||||
archiveCount=$(grep -c . <<< "$archiveList" || true)
|
||||
|
||||
printRow
|
||||
|
||||
local i=0 num dir
|
||||
while read -r dir; do
|
||||
((++i))
|
||||
num=$(printf "%0${#archiveCount}d" "$i")
|
||||
printDotText "$num: $fontBlue$dir$fontReset" "${fontGreen}archive$fontReset"
|
||||
done < <(awk 'NF' <<< "$archiveList")
|
||||
|
||||
printRow
|
||||
local input item selected
|
||||
read -r -p "Specify the backup number: " input
|
||||
|
||||
printRow
|
||||
[[ -z "$input" ]] && input=0
|
||||
[[ "$input" =~ ^[0-9]+$ ]] || { printDanger "Invalid backup number"; return 1; }
|
||||
item=$((10#$input))
|
||||
selected=$(awk 'NF {n++} n == item {print; exit}' item="$item" <<< "$archiveList")
|
||||
[[ -n "$selected" ]] || { printDanger "Unknown backup number"; return 1; }
|
||||
|
||||
cmdStoragePathSync "$backupArchivePath/$selected"
|
||||
}
|
||||
|
||||
# Interactively selects a local daily backup directory and syncs it to external storage.
|
||||
function cmdStorageBackupDailySync() {
|
||||
local dirList error dailyList dailyCount
|
||||
run dirList error fileList "$backupDailyPath" d || { printDanger "$error"; return 1; }
|
||||
dailyList=$(printf '%s\n' "$dirList" | grep -E -- "$backupDailyDirPattern" | sort || true)
|
||||
dailyCount=$(grep -c . <<< "$dailyList" || true)
|
||||
|
||||
printRow
|
||||
|
||||
local i=0 num dir
|
||||
while read -r dir; do
|
||||
((++i))
|
||||
num=$(printf "%0${#dailyCount}d" "$i")
|
||||
printDotText "$num: $fontBlue$dir$fontReset" "${fontGreen}daily$fontReset"
|
||||
done < <(awk 'NF' <<< "$dailyList")
|
||||
|
||||
printRow
|
||||
local input item selected
|
||||
read -r -p "Specify the backup number: " input
|
||||
|
||||
printRow
|
||||
[[ -z "$input" ]] && input=0
|
||||
[[ "$input" =~ ^[0-9]+$ ]] || { printDanger "Invalid backup number"; return 1; }
|
||||
item=$((10#$input))
|
||||
selected=$(awk 'NF {n++} n == item {print; exit}' item="$item" <<< "$dailyList")
|
||||
[[ -n "$selected" ]] || { printDanger "Unknown backup number"; return 1; }
|
||||
|
||||
cmdStoragePathSync "$backupDailyPath/$selected"
|
||||
}
|
||||
|
||||
# Dispatches interactive backup sync by type.
|
||||
# $1 (type): backup type: archive or daily.
|
||||
function cmdStorageBackupSync() {
|
||||
local type
|
||||
type="$1"
|
||||
|
||||
case "$type" in
|
||||
archive)
|
||||
cmdStorageBackupArchiveSync
|
||||
;;
|
||||
daily)
|
||||
cmdStorageBackupDailySync
|
||||
;;
|
||||
*)
|
||||
printDanger "Unknown type backup: $type";
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Interactively selects a backup directory on external storage and removes it.
|
||||
function cmdStorageBackupRemove() {
|
||||
local dirList error dirCount archiveList dailyList dir i=0 num label
|
||||
run dirList error storageFileList "/" d || { printDanger "$error"; return 1; }
|
||||
|
||||
dirCount=$(grep -c . <<< "$dirList" || true)
|
||||
archiveList=$(printf '%s\n' "$dirList" | grep -E -- "$backupArchiveDirPattern" | sort || true)
|
||||
dailyList=$(printf '%s\n' "$dirList" | grep -E -- "$backupDailyDirPattern" | sort || true)
|
||||
|
||||
printRow
|
||||
|
||||
while read -r dir; do
|
||||
((++i))
|
||||
num=$(printf "%0${#dirCount}d" "$i")
|
||||
label="$num: $fontBlue$dir$fontReset"
|
||||
|
||||
if listContains "$dir" "$archiveList"; then
|
||||
printDotText "$label" "${fontGreen}archive$fontReset"
|
||||
elif listContains "$dir" "$dailyList"; then
|
||||
printDotText "$label" "${fontGreen}daily$fontReset"
|
||||
else
|
||||
printDotText "$label" "$labelUnknown"
|
||||
fi
|
||||
done < <(awk 'NF' <<< "$dirList")
|
||||
|
||||
printRow
|
||||
local input item selected
|
||||
read -r -p "Specify the backup number: " input
|
||||
|
||||
printRow
|
||||
[[ -z "$input" ]] && input=0
|
||||
[[ "$input" =~ ^[0-9]+$ ]] || { printDanger "Invalid backup number"; return 1; }
|
||||
item=$((10#$input))
|
||||
selected=$(awk 'NF {n++} n == item {print; exit}' item="$item" <<< "$dirList")
|
||||
[[ -n "$selected" ]] || { printDanger "Unknown backup number"; return 1; }
|
||||
|
||||
if ! runError error storageBackupRemove "$selected"; then
|
||||
printDotText "$selected" "$labelFail"
|
||||
printDanger "$error"
|
||||
return 1
|
||||
fi
|
||||
printDotText "$selected" "$labelDone"
|
||||
}
|
||||
|
||||
# Compares local and remote backup directories, showing which exist on each side.
|
||||
function cmdStorageBackupCompare() {
|
||||
local dirList error localArchiveList localDailyList remoteArchiveList remoteDailyList
|
||||
|
||||
run dirList error fileList "$backupArchivePath" d || { printDanger "Archive path: $error"; return 1; }
|
||||
localArchiveList=$(printf '%s\n' "$dirList" | grep -E -- "$backupArchiveDirPattern" | sort || true)
|
||||
|
||||
run dirList error fileList "$backupDailyPath" d || { printDanger "Archive path: $error"; return 1; }
|
||||
localDailyList=$(printf '%s\n' "$dirList" | grep -E -- "$backupDailyDirPattern" | sort || true)
|
||||
|
||||
run dirList error storageFileList "/" d || { printDanger "$error"; return 1; }
|
||||
remoteArchiveList=$(printf '%s\n' "$dirList" | grep -E -- "$backupArchiveDirPattern" | sort || true)
|
||||
remoteDailyList=$(printf '%s\n' "$dirList" | grep -E -- "$backupDailyDirPattern" | sort || true)
|
||||
|
||||
printSection "Local"
|
||||
printDotText "Archive" "$(grep -c . <<< "$localArchiveList" || true)"
|
||||
printDotText "Daily" "$(grep -c . <<< "$localDailyList" || true)"
|
||||
|
||||
printSection "Remote"
|
||||
printDotText "Archive" "$(grep -c . <<< "$remoteArchiveList" || true)"
|
||||
printDotText "Daily" "$(grep -c . <<< "$remoteDailyList" || true)"
|
||||
|
||||
local allDirs
|
||||
allDirs=$(printf '%s\n' "$localArchiveList" "$localDailyList" "$remoteArchiveList" "$remoteDailyList" | awk 'NF' | sort -u)
|
||||
|
||||
printSection "Comparison"
|
||||
local dir localStatus remoteStatus
|
||||
out="${fontRed}X$fontReset"
|
||||
localIn="${fontGreen}L$fontReset"
|
||||
remoteIn="${fontGreen}R$fontReset"
|
||||
while read -r dir; do
|
||||
localStatus="$out"
|
||||
remoteStatus="$out"
|
||||
|
||||
if listContains "$dir" "$localArchiveList"; then
|
||||
localStatus="$localIn"
|
||||
elif listContains "$dir" "$localDailyList"; then
|
||||
localStatus="$localIn"
|
||||
fi
|
||||
if listContains "$dir" "$remoteArchiveList"; then
|
||||
remoteStatus="$remoteIn"
|
||||
elif listContains "$dir" "$remoteDailyList"; then
|
||||
remoteStatus="$remoteIn"
|
||||
fi
|
||||
|
||||
printDotText "$fontBlue$dir$fontReset" "[ $localStatus : $remoteStatus ]"
|
||||
done < <(awk 'NF' <<< "$allDirs")
|
||||
}
|
||||
|
||||
function cmdStorageBackupSize() {
|
||||
local fileList file size total
|
||||
|
||||
printSection "FTP: $ftpPath"
|
||||
|
||||
total=0
|
||||
fileList=$(ftpFileList | sort -n)
|
||||
while IFS= read -r file; do
|
||||
size=$(ftpPathSize "/$file" "gb")
|
||||
printDotText "$file" "$size Gb"
|
||||
(( total += size ))
|
||||
done <<< "$fileList"
|
||||
|
||||
printRow
|
||||
printDotText "total" "$total Gb"
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
systemLabel="[System]"
|
||||
|
||||
# Runs the full system installation flow.
|
||||
function cmdInstallFull() {
|
||||
systemApt || return 1
|
||||
systemIpset || return 1
|
||||
systemIptables || return 1
|
||||
|
||||
cmdK3sInstall || return 1
|
||||
cmdK3sNamespaceAdd || return 1
|
||||
|
||||
cmdMariadbInstall || return 1
|
||||
cmdRedisInstall || return 1
|
||||
cmdOpenlitespeedInstall || return 1
|
||||
cmdPostfixInstall || return 1
|
||||
cmdWorkerInstall || return 1
|
||||
cmdMetricInstall || return 1
|
||||
}
|
||||
|
||||
# Displays numbered cron job list and stores selected job + current crontab in namerefs.
|
||||
# Known jobs (cronJobs[]): green if installed, gray if missing.
|
||||
# Unknown kube.sh jobs found in crontab: yellow, numbered after known jobs.
|
||||
# $1 (varname): nameref for selected job string
|
||||
# $2 (listvar): nameref for current crontab lines
|
||||
function cmdCronSelect() {
|
||||
local -n __cronJob="$1"
|
||||
local -n __cronList="$2"
|
||||
|
||||
printRow
|
||||
|
||||
local error
|
||||
run __cronList error systemCronList || { printDanger "Error retrieving the CRON job list: $error"; return 1; }
|
||||
|
||||
local -a shownJobs
|
||||
local i job fontColor
|
||||
for ((i=0; i<${#cronJobs[@]}; i++)); do
|
||||
job="${cronJobs[$i]}"
|
||||
shownJobs+=("$job")
|
||||
grep -Fxq -- "$job" <<< "$__cronList" && fontColor="$fontGreen" || fontColor="$fontGray"
|
||||
printf "%2d: %s\n" "$((i+1))" "$fontColor$job$fontReset"
|
||||
done
|
||||
while IFS= read -r job; do
|
||||
[[ -z "$job" ]] && continue
|
||||
grep -Fxq -- "$job" <(printf '%s\n' "${cronJobs[@]}") && continue
|
||||
grep -Fq -- "$appPath/kube.sh" <<< "$job" || continue
|
||||
shownJobs+=("$job")
|
||||
printf "%2d: %s\n" "${#shownJobs[@]}" "$fontYellow$job$fontReset"
|
||||
done <<< "$__cronList"
|
||||
|
||||
printRow
|
||||
local input item
|
||||
read -r -p "Specify the job number: " input
|
||||
|
||||
printRow
|
||||
[[ -z "$input" ]] && input=0
|
||||
[[ "$input" =~ ^[0-9]+$ ]] || { printDanger "Invalid job number"; return 1; }
|
||||
item=$((10#$input - 1))
|
||||
(( item < 0 || item >= ${#shownJobs[@]} )) && { printDanger "Unknown job number"; return 1; }
|
||||
|
||||
__cronJob="${shownJobs[$item]}"
|
||||
}
|
||||
|
||||
# Interactively selects and adds a managed cron job to root crontab.
|
||||
function cmdCronAdd() {
|
||||
local selected jobList
|
||||
cmdCronSelect selected jobList || return 1
|
||||
printDotText "job" "$selected"
|
||||
|
||||
grep -Fxq -- "$selected" <(printf '%s\n' "${cronJobs[@]}") || {
|
||||
printDotText "adding" "$labelFail"
|
||||
printDanger "Cannot add unmanaged job"
|
||||
return 1
|
||||
}
|
||||
|
||||
if grep -Fxq -- "$selected" <<< "$jobList"; then
|
||||
printDotText "adding" "$labelDone"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local tmp
|
||||
tmp=$(mktemp) || return 1
|
||||
{ printf '%s\n' "$jobList"; printf '%s\n' "$selected"; } > "$tmp"
|
||||
crontab -u root "$tmp" || {
|
||||
rm -f "$tmp"
|
||||
printDotText "adding" "$labelFail"
|
||||
return 1
|
||||
}
|
||||
|
||||
rm -f "$tmp"
|
||||
printDotText "adding" "$labelDone"
|
||||
}
|
||||
|
||||
# Interactively selects and removes a managed cron job from root crontab.
|
||||
function cmdCronRemove() {
|
||||
local selected jobList
|
||||
cmdCronSelect selected jobList || return 1
|
||||
printDotText "job" "$selected"
|
||||
|
||||
if ! grep -Fxq -- "$selected" <<< "$jobList"; then
|
||||
printDotText "removing" "$labelDone"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local tmp
|
||||
tmp=$(mktemp) || return 1
|
||||
grep -Fxv -- "$selected" <<< "$jobList" > "$tmp"
|
||||
crontab -u root "$tmp" || {
|
||||
rm -f "$tmp"
|
||||
printDotText "removing" "$labelFail"
|
||||
return 1
|
||||
}
|
||||
|
||||
rm -f "$tmp"
|
||||
printDotText "removing" "$labelDone"
|
||||
}
|
||||
|
||||
# Shows managed cron jobs; installed entries in green, missing in gray, unknown in yellow.
|
||||
function cmdCronList() {
|
||||
local jobList error job fontColor list=""
|
||||
run jobList error systemCronList || {
|
||||
printDanger "systemCronList: $error";
|
||||
return 1;
|
||||
}
|
||||
for ((i=0; i<${#cronJobs[@]}; i++)); do
|
||||
grep -Fxq -- "${cronJobs[$i]}" <<< "$jobList" && fontColor="$fontGreen" || fontColor="$fontGray"
|
||||
list+=$'\n'"$fontColor${cronJobs[$i]}$fontReset"
|
||||
done
|
||||
while IFS= read -r job; do
|
||||
[[ -z "$job" ]] && continue
|
||||
grep -Fxq -- "$job" <(printf '%s\n' "${cronJobs[@]}") && continue
|
||||
grep -Fq -- "$appPath/kube.sh" <<< "$job" || continue
|
||||
list+=$'\n'"$fontYellow$job$fontReset"
|
||||
done <<< "$jobList"
|
||||
|
||||
printRow
|
||||
local i=0 line total
|
||||
total=$(grep -c . <<< "$list")
|
||||
while IFS= read -r line; do
|
||||
[[ -n "$line" ]] || continue
|
||||
((i++))
|
||||
num=$(printf "%${#total}d" "$i")
|
||||
printDotFix 20 "$num: $line"
|
||||
done <<< "$list"
|
||||
}
|
||||
|
||||
# Lists users in the SFTP access group.
|
||||
function cmdSftpUserList() {
|
||||
local output error
|
||||
|
||||
printRow
|
||||
|
||||
if ! run output error sftpUserList "$@"; then
|
||||
printDanger "$error"
|
||||
else
|
||||
printText "$output"
|
||||
fi
|
||||
}
|
||||
|
||||
# Enables SFTP access for a domain user.
|
||||
function cmdSftpAccessEnable() {
|
||||
local error
|
||||
|
||||
printRow
|
||||
|
||||
if ! runError error sftpAccessEnable "$@"; then
|
||||
printDotText "SFTP access enable" "$labelFail"
|
||||
printDanger "$error"
|
||||
else
|
||||
printDotText "SFTP access enable" "$labelDone"
|
||||
fi
|
||||
}
|
||||
|
||||
# Disables SFTP access for a domain user by removing them from the SFTP group.
|
||||
function cmdSftpAccessDisable() {
|
||||
local error
|
||||
|
||||
printRow
|
||||
|
||||
if ! runError error sftpAccessDisable "$@"; then
|
||||
printDotText "SFTP access enable" "$labelFail"
|
||||
printDanger "$error"
|
||||
else
|
||||
printDotText "SFTP access enable" "$labelDone"
|
||||
fi
|
||||
}
|
||||
|
||||
# Sets the SFTP password for a domain user from site config.
|
||||
function cmdSftpPasswordSet() {
|
||||
local error
|
||||
|
||||
printRow
|
||||
|
||||
if ! runError error sftpPasswordSet "$@"; then
|
||||
printDotText "SFTP password set" "$labelFail"
|
||||
printDanger "$error"
|
||||
else
|
||||
printDotText "SFTP password set" "$labelDone"
|
||||
fi
|
||||
}
|
||||
|
||||
# [WARNING] Patches sshd_config to enable SFTP via internal-sftp with group-based chroot.
|
||||
function cmdSftpAddingSupport() {
|
||||
local error
|
||||
|
||||
printSection "Add SFTP support"
|
||||
if ! runError error sftpAddingSupport; then
|
||||
printDotText "adding" "$labelFail"
|
||||
printDanger "$error"
|
||||
else
|
||||
printDotText "adding" "$labelDone"
|
||||
fi
|
||||
|
||||
printSection "Update fail2ban config"
|
||||
if ! runError error fail2banConfigUpdate; then
|
||||
printDotText "updating" "$labelFail"
|
||||
printDanger "$error"
|
||||
else
|
||||
printDotText "updating" "$labelDone"
|
||||
fi
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
testLabel="[Test]"
|
||||
|
||||
# Detect current master pod by parsing INFO replication.
|
||||
function redisReplicaMasterGet() {
|
||||
local output error
|
||||
if ! run output error k3sPodList "$redisKube"; then
|
||||
appError "$error"
|
||||
return 1
|
||||
fi
|
||||
while read -r pod; do
|
||||
if ! run output error redisPodExecCli "$pod" INFO replication; then
|
||||
appError "$pod | $error"
|
||||
continue
|
||||
fi
|
||||
|
||||
if echo "$output" | grep -q "role:master"; then
|
||||
echo "$pod"
|
||||
return 0
|
||||
fi
|
||||
done < <(awk 'NF' <<<"$output")
|
||||
|
||||
appError "Master node not found"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Test MariaDB replica
|
||||
function testMariadbReplica() {
|
||||
local database=$(uuidgen)
|
||||
|
||||
if ! run output error mariadbMasterRootQuery "CREATE DATABASE \`$database\`;"; then
|
||||
printDanger "$testLabel $mariadbMasterKube | Database: $database | Create: $error"
|
||||
return 1
|
||||
else
|
||||
printSuccess "$testLabel $mariadbMasterKube | Database: $database | Create: OK"
|
||||
fi
|
||||
|
||||
local databaseMaster
|
||||
if ! run databaseMaster error mariadbMasterRootQuery "SHOW DATABASES LIKE '$database';"; then
|
||||
printDanger "$testLabel "$mariadbMasterKube" | Database list: $error"
|
||||
else
|
||||
if [[ "$databaseMaster" == "$database" ]]; then
|
||||
printSuccess "$testLabel $mariadbMasterKube | Database: $database | Exists"
|
||||
else
|
||||
printDanger "$testLabel $mariadbMasterKube | Database: $database | Not found"
|
||||
fi
|
||||
fi
|
||||
|
||||
local databaseSlave
|
||||
if ! run databaseSlave error mariadbSlaveRootQuery "SHOW DATABASES LIKE '$database';"; then
|
||||
printDanger "$testLabel "$mariadbSlaveKube" | Database list: $error"
|
||||
else
|
||||
if [[ "$databaseSlave" == "$database" ]]; then
|
||||
printSuccess "$testLabel $mariadbSlaveKube | Database: $database | Exists"
|
||||
else
|
||||
printDanger "$testLabel $mariadbSlaveKube | Database: $database | Not found"
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! run output error mariadbMasterRootQuery "DROP DATABASE \`$database\`;"; then
|
||||
printDanger "$testLabel $mariadbMasterKube | Database: $database | Drop: $error"
|
||||
return 1
|
||||
else
|
||||
printSuccess "$testLabel $mariadbMasterKube | Database: $database | Drop: OK"
|
||||
fi
|
||||
}
|
||||
|
||||
# Test Redis cluster
|
||||
function testRedisCluster() {
|
||||
local key=$(uuidgen)
|
||||
local value=$(uuidgen)
|
||||
|
||||
local podMaster podList error
|
||||
if ! run podList error k3sPodList "$redisKube"; then
|
||||
printDanger "$testLabel k3sPodList: $error"
|
||||
return 1
|
||||
fi
|
||||
if ! run podMaster error redisReplicaMasterGet; then
|
||||
printDanger "$testLabel redisReplicaMasterGet: $error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! run output error redisPodExecCli "$podMaster" SET "$key" "$value"; then
|
||||
printDanger "$testLabel $podMaster | Key: $key | Set: $error"
|
||||
return 1
|
||||
else
|
||||
printSuccess "$testLabel $podMaster | Key: $key | Set: $value"
|
||||
fi
|
||||
|
||||
while read pod; do
|
||||
if ! run output error redisPodExecCli "$pod" GET "$key"; then
|
||||
printDanger "$testLabel $pod | Key: $key | Get: $error"
|
||||
continue
|
||||
fi
|
||||
if [[ "$output" == "$value" ]]; then
|
||||
printSuccess "$testLabel $pod | Key: $key | Get: $output"
|
||||
else
|
||||
printDanger "$testLabel $pod | Key: $key | Get: $output"
|
||||
fi
|
||||
done < <(awk 'NF' <<<"$podList")
|
||||
|
||||
if ! run output error redisPodExecCli "$podMaster" DEL "$key"; then
|
||||
printDanger "$testLabel $podMaster | Key: $key | Del: $error"
|
||||
return 1
|
||||
else
|
||||
printSuccess "$testLabel $podMaster | Key: $key | Del: OK"
|
||||
fi
|
||||
}
|
||||
|
||||
# Test create site
|
||||
function testSite() {
|
||||
local domain
|
||||
domain="test-$(stringRandom 16 'a-z').test"
|
||||
|
||||
if ! run output error cmdSiteAddDefault "$domain"; then
|
||||
printDanger "$testLabel Domain: $domain | Create: $error"
|
||||
else
|
||||
printSuccess "$testLabel Domain: $domain | Create: OK"
|
||||
cmdOpenlitespeedRestart
|
||||
|
||||
if ! run output error systemHostAdd "$domain"; then
|
||||
printDanger "$testLabel Domain: $domain | Host add: $error"
|
||||
else
|
||||
printSuccess "$testLabel Domain: $domain | Host add: OK"
|
||||
fi
|
||||
|
||||
local code
|
||||
if ! run code error curl -s -L -o /dev/null -w "%{http_code}" "$domain"; then
|
||||
printDanger "$testLabel Domain: $domain | Curl: $code: $error"
|
||||
else
|
||||
if [[ "$code" == "200" ]]; then
|
||||
printSuccess "$testLabel Domain: $domain | Curl: $code"
|
||||
else
|
||||
printWarning "$testLabel Domain: $domain | Curl: $code"
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! run output error systemHostRemove "$domain"; then
|
||||
printDanger "$testLabel Domain: $domain | Host remove: $error"
|
||||
else
|
||||
printSuccess "$testLabel Domain: $domain | Host remove: OK"
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! run output error cmdSiteRemove "$domain"; then
|
||||
printDanger "$testLabel Domain: $domain | Site remove: $error"
|
||||
else
|
||||
printSuccess "$testLabel Domain: $domain | Site remove: OK"
|
||||
fi
|
||||
cmdOpenlitespeedRestart
|
||||
}
|
||||
|
||||
# Test create site Wordpress
|
||||
function testSiteWordpress() {
|
||||
local domain
|
||||
domain="test-$(stringRandom 16 'a-z').test"
|
||||
|
||||
if ! run output error cmdSiteAddWordpress $domain; then
|
||||
printDanger "$testLabel Domain: $domain | Create: $error"
|
||||
else
|
||||
printSuccess "$testLabel Domain: $domain | Create: OK"
|
||||
cmdOpenlitespeedRestart
|
||||
|
||||
if ! run output error systemHostAdd "$domain"; then
|
||||
printDanger "$testLabel Domain: $domain | Host add: $error"
|
||||
else
|
||||
printSuccess "$testLabel Domain: $domain | Host add: OK"
|
||||
fi
|
||||
|
||||
local code
|
||||
if ! run code error curl -s -L -o /dev/null -w "%{http_code}" "$domain"; then
|
||||
printDanger "$testLabel Domain: $domain | Curl: $code: $error"
|
||||
else
|
||||
if [[ "$code" == "200" ]]; then
|
||||
printSuccess "$testLabel Domain: $domain | Curl: $code"
|
||||
else
|
||||
printWarning "$testLabel Domain: $domain | Curl: $code"
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! run output error systemHostRemove "$domain"; then
|
||||
printDanger "$testLabel Domain: $domain | Host remove: $error"
|
||||
else
|
||||
printSuccess "$testLabel Domain: $domain | Host remove: OK"
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! run output error cmdSiteRemove "$domain"; then
|
||||
printDanger "$testLabel Domain: $domain | Site remove: $error"
|
||||
else
|
||||
printSuccess "$testLabel Domain: $domain | Site remove: OK"
|
||||
fi
|
||||
cmdOpenlitespeedRestart
|
||||
}
|
||||
|
||||
|
||||
# testRedisCluster
|
||||
|
||||
|
||||
# local key="kube:haproxy:uuid" uuid
|
||||
# uuid=$(uuidgen)
|
||||
# if run output error redisExecCli -h redis-master -p 6379 SET "$key" "$uuid"; then
|
||||
# printInfo "$redisHaproxyLabel Set $key | $uuid"
|
||||
# else
|
||||
# printDanger "$redisHaproxyLabel Set $key: $error"
|
||||
# fi
|
||||
# if run output error redisExecCli -h redis-master -p 6379 GET "$key"; then
|
||||
# printInfo "$redisHaproxyLabel Get $key | $output"
|
||||
# if [[ "$uuid" != "$output" ]]; then
|
||||
# printDanger "$redisHaproxyLabel Validation failed"
|
||||
# else
|
||||
# printSuccess "$redisHaproxyLabel Validation success"
|
||||
# fi
|
||||
# else
|
||||
# printDanger "$redisHaproxyLabel Get $key: $error"
|
||||
# fi
|
||||
@@ -0,0 +1,59 @@
|
||||
UNIGMA_PHP=""
|
||||
UNIGMA_MEM=""
|
||||
UNIGMA_EXEC=""
|
||||
|
||||
function cmdUnigma() {
|
||||
local podList vhostsList error raw pod phase
|
||||
|
||||
printSection "Unigma"
|
||||
|
||||
run podList error k3sPodListStatus "$olsKube" || { printDanger "Get pods: $error"; return 1; }
|
||||
[[ -n "$podList" ]] || { printDanger "Pods not found"; return 1; }
|
||||
|
||||
# run vhostList error fileList "$olsVhostsPath" d || { printDanger "$error"; return 1; }
|
||||
run vhostList error openlitespeedConfigVhostList || { printDanger "Failed get list of vhosts: $error"; return 1; }
|
||||
while read -r vhost; do
|
||||
run raw error unigmaGetTxt "$vhost" || {
|
||||
printDotText "$vhost" "${fontGray}failed get Unigma data$fontReset";
|
||||
continue
|
||||
}
|
||||
# raw='php=8.1;mem=112M;exec=60'
|
||||
|
||||
unigmaParse "$raw" || {
|
||||
printDotText "$vhost" "$labelFail";
|
||||
printDanger "$error"
|
||||
continue
|
||||
}
|
||||
|
||||
unigmaIniSet "$vhost" "$UNIGMA_MEM" "$UNIGMA_EXEC"
|
||||
case $? in
|
||||
1) printDotText "$vhost" "$labelFail"; printDanger "$error"; continue ;;
|
||||
2)
|
||||
uid=$(domainToUid "$vhost")
|
||||
[[ -n "$uid" ]] || {
|
||||
printDotText "$vhost" "$labelFail";
|
||||
printDanger "Cannot resolve UID for: $vhost";
|
||||
continue;
|
||||
}
|
||||
|
||||
local pod phase
|
||||
while IFS='|' read -r pod phase; do
|
||||
if [[ "$phase" != "Running" ]]; then
|
||||
printDotText " $pod" "$fontRed$phase$fontReset"
|
||||
else
|
||||
runSilent openlitespeedPodExec "$pod" pkill -u "$uid" -x lsphp
|
||||
printDotText " kill$fontBlue lsphp$fontReset processes for $vhost"
|
||||
fi
|
||||
done < <(awk 'NF' <<< "$podList")
|
||||
;;
|
||||
esac
|
||||
|
||||
unigmaPhpSet "$vhost" "$UNIGMA_PHP" || {
|
||||
printDotText "$vhost" "$labelFail";
|
||||
printDanger "$error"
|
||||
continue
|
||||
}
|
||||
|
||||
printDotText "$vhost" "$labelDone";
|
||||
done < <(awk 'NF' <<< "$vhostList")
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
wordpressLabel="[Wordpress]"
|
||||
|
||||
# Lists all WordPress users for a site.
|
||||
# $1 (domain): site domain name.
|
||||
function cmdWordpressUserList() {
|
||||
local output error
|
||||
|
||||
printRow
|
||||
|
||||
if ! run output error wordpressUserList "$@"; then
|
||||
printDanger "$error"
|
||||
else
|
||||
printText "$output"
|
||||
fi
|
||||
}
|
||||
|
||||
# Sets the password for a WordPress user.
|
||||
# $1 (domain): site domain name.
|
||||
# $2 (user): WordPress username or user ID.
|
||||
# $3 (password): new password.
|
||||
function cmdWordpressPasswordSet() {
|
||||
local output error
|
||||
|
||||
printRow
|
||||
|
||||
if ! run output error wordpressPasswordSet "$@"; then
|
||||
printDanger "$error"
|
||||
else
|
||||
printText "$output"
|
||||
fi
|
||||
}
|
||||
|
||||
# Executes an arbitrary WP-CLI command inside the site's vhost or all vhosts.
|
||||
# $1 (all|domain): site domain name or all vhosts.
|
||||
# $@ (...): WP-CLI sub-command and arguments.
|
||||
function cmdWordpressExec() {
|
||||
local target="$1"
|
||||
shift || true
|
||||
|
||||
local vhostList error
|
||||
|
||||
printRow
|
||||
|
||||
if [[ -z "$target" ]]; then
|
||||
printDanger "Target not specified";
|
||||
elif ! run vhostList error openlitespeedConfigVhostList; then
|
||||
printDanger "Cannot get vhost list: $error";
|
||||
elif [[ "$target" == "all" ]]; then
|
||||
local domain
|
||||
for domain in $vhostList; do
|
||||
printSection "$domain"
|
||||
if ! run output error wordpressExec "$domain" "$@"; then
|
||||
printDanger "$error"
|
||||
else
|
||||
printText "$output"
|
||||
fi
|
||||
done
|
||||
elif ! listContains "$target" "$vhostList"; then
|
||||
printDanger "Unknown domain: $target";
|
||||
elif ! run output error wordpressExec "$target" "$@"; then
|
||||
printDanger "$error"
|
||||
else
|
||||
printText "$output"
|
||||
fi
|
||||
}
|
||||
|
||||
function cmdWordpressSalt() {
|
||||
local target="$1"
|
||||
local vhostList error
|
||||
|
||||
printRow
|
||||
|
||||
if [[ -z "$target" ]]; then
|
||||
printDanger "Target not specified";
|
||||
elif ! run vhostList error openlitespeedConfigVhostList; then
|
||||
printDanger "Cannot get vhost list: $error";
|
||||
elif [[ "$target" == "all" ]]; then
|
||||
local domain
|
||||
for domain in $vhostList; do
|
||||
if ! runError error wordpressSalt "$domain"; then
|
||||
printDotText "$domain" "$labelFail"
|
||||
printDanger "$error"
|
||||
else
|
||||
printDotText "$domain" "$labelDone"
|
||||
fi
|
||||
done
|
||||
elif ! listContains "$target" "$vhostList"; then
|
||||
printDanger "Unknown domain: $target";
|
||||
elif ! runError error wordpressSalt "$target"; then
|
||||
printDotText "$target" "$labelFail"
|
||||
printDanger "$error"
|
||||
else
|
||||
printDotText "$target" "$labelDone"
|
||||
fi
|
||||
}
|
||||
|
||||
function cmdWordpressCheck() {
|
||||
local target="$1"
|
||||
local vhostList error
|
||||
|
||||
printRow
|
||||
|
||||
if [[ -z "$target" ]]; then
|
||||
printDanger "Target not specified";
|
||||
elif ! run vhostList error openlitespeedConfigVhostList; then
|
||||
printDanger "Cannot get vhost list: $error";
|
||||
elif [[ "$target" == "all" ]]; then
|
||||
local domain
|
||||
for domain in $vhostList; do
|
||||
printSection "$domain | core"
|
||||
wordpressExec "$domain" core verify-checksums
|
||||
|
||||
printSection "$domain | plugins"
|
||||
wordpressExec "$domain" plugin verify-checksums --all
|
||||
done
|
||||
elif ! listContains "$target" "$vhostList"; then
|
||||
printDanger "Unknown domain: $target";
|
||||
else
|
||||
printSection "$target | core"
|
||||
wordpressExec "$target" core verify-checksums
|
||||
|
||||
printSection "$target | plugins"
|
||||
wordpressExec "$target" plugin verify-checksums --all
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Updates all WordPress URLs in DB to match the current domain, cleans cache and Redis.
|
||||
# $1 (domain): site domain name.
|
||||
# [$2] (abspathOld): old absolute path to replace.
|
||||
function cmdWordpressDomainUpdate() {
|
||||
local domain="$1"
|
||||
[[ -n "$domain" ]] || { printDanger "$wordpressLabel Domain not specified"; return 1; }
|
||||
local siteNew="https://$domain"
|
||||
|
||||
local abspathOld="$2"
|
||||
|
||||
local isMultiSite
|
||||
isMultiSite=$(wordpressExec "$domain" eval 'echo is_multisite() ? 1 : 0;')
|
||||
if [[ "$isMultiSite" == "1" ]]; then
|
||||
printDanger "$wordpressLabel This script is for SINGLE SITE only. Detected multisite. Abort."
|
||||
return 1
|
||||
fi
|
||||
|
||||
local siteConst homeConst siteOption homeOption
|
||||
siteConst=$(wordpressExec "$domain" eval 'echo defined("WP_SITEURL")?WP_SITEURL:"";' || true)
|
||||
siteConst=$(strTrimSlash "$siteConst")
|
||||
homeConst=$(wordpressExec "$domain" eval 'echo defined("WP_HOME")?WP_HOME:"";' || true)
|
||||
homeConst=$(strTrimSlash "$homeConst")
|
||||
siteOption=$(wordpressExec "$domain" option get siteurl 2>/dev/null || true)
|
||||
siteOption=$(strTrimSlash "$siteOption")
|
||||
homeOption=$(wordpressExec "$domain" option get home 2>/dev/null || true)
|
||||
homeOption=$(strTrimSlash "$homeOption")
|
||||
printInfo "$wordpressLabel Wordpress siteurl | Const: $siteConst | Option: $siteOption"
|
||||
printInfo "$wordpressLabel Wordpress home | Const: $homeConst | Option: $homeOption"
|
||||
|
||||
local siteOld="${siteConst:-$siteOption}"
|
||||
if [[ -z "$siteOld" ]]; then
|
||||
siteOld="${homeConst:-$homeOption}"
|
||||
fi
|
||||
if [[ -z "$siteOld" ]]; then
|
||||
appError "Could not detect siteOld (neither constants nor DB options present)."
|
||||
return 2
|
||||
fi
|
||||
local schemeOld hostOld rootOld
|
||||
schemeOld=$(printf '%s' "$siteOld" | awk -F:// '{print $1}')
|
||||
hostOld=$(printf '%s' "$siteOld" | awk -F[/:] '{print $4}')
|
||||
rootOld="$schemeOld://$hostOld"
|
||||
printInfo "$wordpressLabel Wordpress OLD | Site: $siteOld | Scheme: $schemeOld | Host: $hostOld | Root: $rootOld"
|
||||
|
||||
local sitePath homePath
|
||||
sitePath=$(wordpressExec "$domain" eval 'echo parse_url(get_option("siteurl"), PHP_URL_PATH)?:"";' || true)
|
||||
[[ "$sitePath" == "/" ]] && sitePath=""
|
||||
homePath=$(wordpressExec "$domain" eval 'echo parse_url(get_option("home"), PHP_URL_PATH)?:"";' || true)
|
||||
[[ "$homePath" == "/" ]] && homePath=""
|
||||
|
||||
local wpType="unknown"
|
||||
if [[ -z "$homePath" && -z "$sitePath" ]]; then wpType="single_root"
|
||||
elif [[ -n "$homePath" && -n "$sitePath" && "$homePath" == "$sitePath" ]]; then wpType="single_subdir"
|
||||
elif [[ -z "$homePath" && -n "$sitePath" ]]; then wpType="single_mixed"
|
||||
fi
|
||||
printInfo "$wordpressLabel Wordpress OLD | Type: $wpType"
|
||||
|
||||
local siteConstHas homeConstHas
|
||||
siteConstHas=$(wordpressExec "$domain" eval 'echo defined("WP_SITEURL")?1:0;')
|
||||
if [[ "$siteConstHas" == "1" ]]; then
|
||||
wordpressExec "$domain" config delete WP_SITEURL --type=constant || true
|
||||
fi
|
||||
homeConstHas=$(wordpressExec "$domain" eval 'echo defined("WP_HOME")?1:0;')
|
||||
if [[ "$homeConstHas" == "1" ]]; then
|
||||
wordpressExec "$domain" config delete WP_HOME --type=constant || true
|
||||
fi
|
||||
|
||||
printInfo "$wordpressLabel Update siteurl: $siteNew"
|
||||
wordpressExec "$domain" option update siteurl "$siteNew"
|
||||
|
||||
printInfo "$wordpressLabel Update home: $siteNew"
|
||||
wordpressExec "$domain" option update home "$siteNew"
|
||||
|
||||
printInfo "$wordpressLabel Replace in DB (1): $siteOld --> $siteNew"
|
||||
wordpressSearchReplace "$domain" "$siteOld" "$siteNew"
|
||||
if [[ -n "$homeOption" && "$homeOption" != "$siteOld" ]]; then
|
||||
printInfo "$wordpressLabel Replace in DB (2): $homeOption --> $siteNew"
|
||||
wordpressSearchReplace "$domain" "$homeOption" "$siteNew"
|
||||
fi
|
||||
if [[ -n "$siteOption" && "$siteOption" != "$siteOld" && "$siteOption" != "$homeOption" ]]; then
|
||||
printInfo "$wordpressLabel Replace in DB (3): $siteOption --> $siteNew"
|
||||
wordpressSearchReplace "$domain" "$siteOption" "$siteNew"
|
||||
fi
|
||||
if [[ -n "$hostOld" ]]; then
|
||||
printInfo "$wordpressLabel Replace in DB (4): //$hostOld --> $siteNew"
|
||||
wordpressSearchReplace "$domain" "//$hostOld" "$siteNew"
|
||||
fi
|
||||
if [[ "$wpType" == "single_mixed" ]]; then
|
||||
printInfo "$wordpressLabel Replace in DB (5): $rootOld --> $siteNew"
|
||||
wordpressSearchReplace "$domain" "$rootOld" "$siteNew"
|
||||
fi
|
||||
if [[ -n "$abspathOld" ]]; then
|
||||
printInfo "$wordpressLabel Replace in DB (6): $abspathOld --> $olsPodVhostsPath/$domain/www"
|
||||
wordpressSearchReplace "$domain" "$abspathOld" "$olsPodVhostsPath/$domain/www"
|
||||
fi
|
||||
|
||||
printInfo "$wordpressLabel Replace in DB (7): $hostOld --> $domain"
|
||||
wordpressSearchReplace "$domain" "$hostOld" "$domain"
|
||||
|
||||
printInfo "$wordpressLabel Delete options: upload_path/upload_url_path..."
|
||||
wordpressExec "$domain" option delete upload_path || true
|
||||
wordpressExec "$domain" option delete upload_url_path || true
|
||||
|
||||
printInfo "$wordpressLabel Clean cache..."
|
||||
wordpressExec "$domain" transient delete --all || true
|
||||
|
||||
printInfo "$wordpressLabel Redis clean..."
|
||||
redisDomainClean "$domain" || true
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
workerLabel="[Worker]"
|
||||
taskStatusExecution="execution"
|
||||
taskStatusSuccess="success"
|
||||
taskStatusFailed="failed"
|
||||
taskStatusWaiting="waiting"
|
||||
taskStatusNew="new"
|
||||
|
||||
# Prepares worker agent directory and UUID file.
|
||||
function cmdWorkerPreparation() {
|
||||
printSection "Preparation..."
|
||||
|
||||
if [[ ! -f "$workerAgentPath/worker.py" ]]; then
|
||||
mkdir -p "$workerAgentPath"
|
||||
cp -f -- "$appAssetsPath/$workerKube/worker.py" "$workerAgentPath/worker.py"
|
||||
fi
|
||||
rm -f "$workerAgentPath/worker.uuid"
|
||||
fileValueGetOrCreate "$workerAgentPath/worker.uuid" "$hostUuid" >/dev/null 2>&1 || {
|
||||
printDotText "preparation" "$labelFail"
|
||||
printDanger "Generation UUID failed";
|
||||
return 1;
|
||||
}
|
||||
|
||||
printDotText "preparation" "$labelDone"
|
||||
}
|
||||
|
||||
# Renders the Worker Kubernetes YAML manifest via Helm.
|
||||
function cmdWorkerYamlRender() {
|
||||
local templateFile="templates/$workerKube.yaml"
|
||||
|
||||
printSection "Render YAML file | Helm"
|
||||
printDotText "Template" "$appAssetsPath/k3s/$templateFile"
|
||||
[[ -f "$appAssetsPath/k3s/$templateFile" ]] || {
|
||||
printDanger "Template file not found"
|
||||
return 1
|
||||
}
|
||||
|
||||
local varsFile
|
||||
varsFile=$(mktemp "/tmp/$workerKube.vars.XXXXXX.yaml") || {
|
||||
printDotText "render" "$labelFail"
|
||||
printDanger "Create temp variables file"
|
||||
return 1
|
||||
}
|
||||
printDotText "Variables" "$varsFile"
|
||||
trap 'rm -f -- "$varsFile"' RETURN
|
||||
cat > "$varsFile" <<EOF
|
||||
workerHelm: true
|
||||
namespace: $k3sNamespace
|
||||
worker: $workerKube
|
||||
workerAgentPath: $workerAgentPath
|
||||
workerTasksPath: $workerTasksPath
|
||||
workerUuid: $hostUuid
|
||||
workerName: $workerName
|
||||
workerPool: $workerPool
|
||||
workerApiUrl: $workerApiUrl
|
||||
workerApiGettingPause: $workerApiGettingPause
|
||||
workerApiSendingPause: $workerApiSendingPause
|
||||
EOF
|
||||
|
||||
printDotText "Result" "$workerYaml"
|
||||
mkdir -p -- "$(dirname -- "$workerYaml")" || return 1
|
||||
fileBackup "$workerYaml"
|
||||
helm template stack "$appAssetsPath/k3s" -f "$varsFile" --show-only "$templateFile" > "$workerYaml" || {
|
||||
printDotText "render" "$labelFail"
|
||||
printDanger "Render failed"
|
||||
return 1
|
||||
}
|
||||
|
||||
printDotText "render" "$labelDone"
|
||||
}
|
||||
|
||||
function cmdWorkerUpdate() {
|
||||
cmdWorkerYamlRender || return 1
|
||||
cmdK3sYamlApplyEx "$workerYaml" || return 1
|
||||
}
|
||||
|
||||
# Installs Worker into Kubernetes.
|
||||
function cmdWorkerInstall() {
|
||||
cmdWorkerPreparation || return 1
|
||||
cmdWorkerYamlRender || return 1
|
||||
cmdK3sYamlApplyEx "$workerYaml" || return 1
|
||||
}
|
||||
|
||||
# Uninstalls Worker Kubernetes resources.
|
||||
function cmdWorkerUninstall() {
|
||||
"$k3sCmd" kubectl delete -f "$workerYaml"
|
||||
}
|
||||
|
||||
# Executes one worker task described in an INI-like config file.
|
||||
# $1 (taskFile): path to the task config file.
|
||||
# [$2] (domain): override domain (read from task file if omitted).
|
||||
function cmdWorkerTaskHandle() {
|
||||
local taskFile="$1"
|
||||
[[ -n "$taskFile" ]] || { printDanger "$workerLabel Task file not specified"; return 1; }
|
||||
[[ -f "$taskFile" ]] || { printDanger "$workerLabel Task: $taskFile | File not found"; return 1; }
|
||||
printInfo "$workerLabel Task: $taskFile"
|
||||
|
||||
local domain="$2"
|
||||
[[ -n "$domain" ]] || domain=$(configGet "$taskFile" "domain")
|
||||
|
||||
local status
|
||||
status=$(configGet "$taskFile" "status")
|
||||
if [[ "$status" != "$taskStatusNew" && "$status" != "$taskStatusWaiting" ]]; then
|
||||
printDanger "$workerLabel Task: $taskFile | Status not '$taskStatusNew' or '$taskStatusWaiting'"
|
||||
return 1
|
||||
fi
|
||||
configSet "$taskFile" "status" "$taskStatusExecution"
|
||||
configSet "$taskFile" "start" "$(date +%s)"
|
||||
|
||||
local action
|
||||
action=$(configGet "$taskFile" "action")
|
||||
printInfo "$workerLabel Action: $action"
|
||||
case "$action" in
|
||||
"copy")
|
||||
local databaseUrl
|
||||
databaseUrl=$(configGet "$taskFile" "database_url")
|
||||
if ! run output error urlAccessible "$databaseUrl"; then
|
||||
printDanger "$workerLabel URL database archive is invalid: $databaseUrl"
|
||||
configSet "$taskFile" "status" "$taskStatusFailed"
|
||||
configSet "$taskFile" "message" "URL database archive is invalid: $databaseUrl"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local filesUrl
|
||||
filesUrl=$(configGet "$taskFile" "files_url")
|
||||
if ! run output error urlAccessible "$filesUrl"; then
|
||||
printDanger "$workerLabel URL files archive is invalid: $filesUrl"
|
||||
configSet "$taskFile" "status" "$taskStatusFailed"
|
||||
configSet "$taskFile" "message" "URL files archive is invalid: $filesUrl"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -z "$domain" ]]; then
|
||||
domain=$(domainsListMark "$domainsList")
|
||||
fi
|
||||
if [[ -z "$domain" ]]; then
|
||||
printDanger "$workerLabel Domain not specified or no domains available"
|
||||
configSet "$taskFile" "status" "$taskStatusFailed"
|
||||
configSet "$taskFile" "message" "Domain not specified or no domains available"
|
||||
return 1
|
||||
else
|
||||
configSet "$taskFile" "domain" "$domain"
|
||||
fi
|
||||
|
||||
configSet "$taskFile" "status" "$taskStatusExecution"
|
||||
|
||||
mkdir -p "$workerFilesPath"
|
||||
local fileName filesLocal databaseLocal
|
||||
fileName="${domain}_$(uuidgen)"
|
||||
|
||||
databaseLocal="$workerFilesPath/$(urlLocalFileGen "$databaseUrl" "$fileName")"
|
||||
printInfo "$workerLabel Download archive database --> $databaseLocal"
|
||||
if ! run output error fileDownload "$databaseUrl" "$databaseLocal"; then
|
||||
printDanger "$error"
|
||||
configSet "$taskFile" "status" "$taskStatusFailed"
|
||||
configSet "$taskFile" "message" "$error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
filesLocal="$workerFilesPath/$(urlLocalFileGen "$filesUrl" "$fileName")"
|
||||
printInfo "$workerLabel Download archive files --> $filesLocal"
|
||||
if ! run output error fileDownload "$filesUrl" "$filesLocal"; then
|
||||
printDanger "$error"
|
||||
configSet "$taskFile" "status" "$taskStatusFailed"
|
||||
configSet "$taskFile" "message" "$error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
printInfo "$workerLabel Create site: $domain"
|
||||
if ! run output error cmdSiteAddWordpress "$domain" "$filesLocal" "$databaseLocal"; then
|
||||
printDanger "$error"
|
||||
configSet "$taskFile" "status" "$taskStatusFailed"
|
||||
configSet "$taskFile" "message" "Error create site: $error"
|
||||
return 1
|
||||
else
|
||||
cmdWordpressDomainUpdate "$domain"
|
||||
cmdOpenlitespeedRestart
|
||||
fi
|
||||
;;
|
||||
"pack")
|
||||
local output error
|
||||
local uuid="worker_$(uuidgen)"
|
||||
|
||||
if ! run vhostList error openlitespeedConfigVhostList; then
|
||||
printDanger "Vhost list: $error"
|
||||
configSet "$taskFile" "status" "$taskStatusFailed"
|
||||
configSet "$taskFile" "message" "Error getting list of virtual hosts"
|
||||
return 1
|
||||
elif ! listContains "$domain" "$vhostList"; then
|
||||
printDanger "Vhost list: Domain is not exists in OpenLiteSpeed config"
|
||||
configSet "$taskFile" "status" "$taskStatusFailed"
|
||||
configSet "$taskFile" "message" "Domain is not exists in OpenLiteSpeed config"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! run output error backupSiteFiles "$domain" "$olsVhostsPath/$domain/www/$uuid"; then
|
||||
printDanger "$error"
|
||||
configSet "$taskFile" "status" "$taskStatusFailed"
|
||||
configSet "$taskFile" "message" "Error create files archive: $error"
|
||||
return 1
|
||||
fi
|
||||
if ! run output error backupSiteDatabase "$domain" "$olsVhostsPath/$domain/www/$uuid.sql"; then
|
||||
printDanger "$error"
|
||||
configSet "$taskFile" "status" "$taskStatusFailed"
|
||||
configSet "$taskFile" "message" "Error create database archive: $error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
configSet "$taskFile" "files_url" "https://$domain/$uuid.tar.gz"
|
||||
configSet "$taskFile" "database_url" "https://$domain/$uuid.sql.tar.gz"
|
||||
;;
|
||||
"delete")
|
||||
printSuccess "$workerLabel Action: Site delete..."
|
||||
cmdSiteRemove "$domain"
|
||||
cmdOpenlitespeedRestart
|
||||
;;
|
||||
"update")
|
||||
domain=$(configGet "$taskFile" "domain")
|
||||
if [[ -z "$domain" ]]; then
|
||||
printDanger "$workerLabel Domain not specified"
|
||||
configSet "$taskFile" "status" "$taskStatusFailed"
|
||||
configSet "$taskFile" "message" "Domain not specified"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local domainList
|
||||
domainList=$(postfixDomainList)
|
||||
if ! listContains "$domain" "$domainList"; then
|
||||
printDanger "$workerLabel Domain $domain not found"
|
||||
configSet "$taskFile" "status" "$taskStatusFailed"
|
||||
configSet "$taskFile" "message" "Domain $domain not found"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local postfixForwardTo
|
||||
postfixForwardTo=$(configGet "$taskFile" "mail_forward_to")
|
||||
siteConfigSet "$domain" "postfixForwardTo" "$postfixForwardTo"
|
||||
postfixVirtualAliasSet "@$domain" "$postfixForwardTo"
|
||||
postfixPostmapRebuild
|
||||
;;
|
||||
*)
|
||||
printDanger "$workerLabel Unknown action: $action"
|
||||
configSet "$taskFile" "status" "$taskStatusFailed"
|
||||
configSet "$taskFile" "message" "Unknown action: $action"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
configSet "$taskFile" "status" "$taskStatusSuccess"
|
||||
printSuccess "$workerLabel Action: $action | Success"
|
||||
|
||||
local taskFileBase
|
||||
taskFileBase=$(basename -- "$taskFile")
|
||||
mkdir -p "$appDataPath/worker-task" || true
|
||||
cp -f -- "$taskFile" "$appDataPath/worker-task/$taskFileBase" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Scans task dir and runs handler for tasks with status new or waiting.
|
||||
function cmdWorkerObserver() {
|
||||
local fileList error
|
||||
run fileList error fileList "$workerTasksPath" f || { appError "$error"; return 1; }
|
||||
while IFS= read -r file; do
|
||||
local taskFile="$workerTasksPath/$file"
|
||||
local status
|
||||
status=$(configGet "$taskFile" "status")
|
||||
if [[ "$status" == "$taskStatusNew" || "$status" == "$taskStatusWaiting" ]]; then
|
||||
printSuccess "$workerLabel $file | Status: $status | Starting..."
|
||||
cmdWorkerTaskHandle "$taskFile"
|
||||
else
|
||||
printInfo "$workerLabel $file | Status: $status | Skip"
|
||||
fi
|
||||
done < <(awk 'NF' <<< "$fileList")
|
||||
}
|
||||
|
||||
# Lists worker tasks with action, status, and lifetime in seconds.
|
||||
function cmdWorkerTaskList() {
|
||||
local fileList error
|
||||
run fileList error fileList "$workerTasksPath" f || {
|
||||
printDanger "$error";
|
||||
return 1;
|
||||
}
|
||||
|
||||
local count=0
|
||||
while IFS= read -r file; do
|
||||
((count++))
|
||||
local task=${file%.task}
|
||||
local action status start
|
||||
action=$(configGet "$workerTasksPath/$file" "action")
|
||||
status=$(configGet "$workerTasksPath/$file" "status")
|
||||
start=$(configGet "$workerTasksPath/$file" "start")
|
||||
local live="?"
|
||||
if [[ -n "$start" ]]; then
|
||||
live=$(( $(date +%s) - start ))
|
||||
fi
|
||||
|
||||
printSection "$task"
|
||||
printDotText "file" "$file"
|
||||
printDotText "action" "$action"
|
||||
printDotText "status" "$status"
|
||||
printDotText "live, sec" "$live"
|
||||
done < <(awk 'NF' <<< "$fileList")
|
||||
}
|
||||
@@ -0,0 +1,726 @@
|
||||
# Searches for entries (files and directories) in the specified local directory.
|
||||
#
|
||||
# $1 (path): The path to the directory on the local machine where the search for entries will be performed.
|
||||
# $2 (type): Type entry (f: files, d: directories, ...).
|
||||
function fileList() {
|
||||
local path="${1-}"
|
||||
local type="${2-}"
|
||||
local args=(-mindepth 1 -maxdepth 1)
|
||||
|
||||
[[ -n "$path" ]] || { appError "Path not specified"; return 1; }
|
||||
[[ -d "$path" ]] || { appError "Directory not found: $path"; return 1; }
|
||||
|
||||
if [[ -n "$type" ]]; then
|
||||
case "$type" in
|
||||
f|d|l|p|s|b|c) args+=(-type "$type") ;;
|
||||
*) appError "Unsupported file type: $type"; return 1 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
find "$path" "${args[@]}" -printf '%f\n'
|
||||
}
|
||||
|
||||
# Searches for entries (files or directories) in the specified directory on an FTP server.
|
||||
#
|
||||
# $1 (path): The path to the directory on the FTP server where the search for entries will be performed.
|
||||
# $2 (type): Type entry (f: files, d: directories, ...).
|
||||
function ftpFileList() {
|
||||
local path="$1"
|
||||
local type="$2"
|
||||
local output error
|
||||
|
||||
run output error curl -sS -u "$ftpUser:$ftpPass" "ftp://$ftpHost:$ftpPort$ftpPath$path/" --connect-timeout 10 \
|
||||
|| { appError "$error"; return 1; }
|
||||
|
||||
case "$type" in
|
||||
f) printf '%s\n' "$output" | grep -v '^d' | awk '{print $NF}' ;;
|
||||
d) printf '%s\n' "$output" | grep '^d' | awk '{print $NF}' ;;
|
||||
*) printf '%s\n' "$output" | awk '{print $NF}' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Searches for entries (files or directories) in the specified directory on a remote server via SSH.
|
||||
#
|
||||
# $1 (path): The path to the directory on the remote server where the search for entries will be performed.
|
||||
# $2 (type): Type entry (f: files, d: directories, ...).
|
||||
function sshFileList() {
|
||||
local path="$1"
|
||||
local type="$2"
|
||||
local typeArg="${type:+-type $type}"
|
||||
local output error
|
||||
|
||||
run output error ssh -i "$sshKeyFile" "$sshUser@$sshHost" \
|
||||
"find '${sshPath}${path}' -maxdepth 1 -mindepth 1 ${typeArg} -exec basename {} \\;" \
|
||||
|| { appError "$error"; return 1; }
|
||||
|
||||
printf '%s\n' "$output"
|
||||
}
|
||||
|
||||
# Searches for entries (files or directories) in the specified directory on external storage.
|
||||
function storageFileList() {
|
||||
local -A storageFunc=(
|
||||
[ftp]=ftpFileList
|
||||
[rsync]=ftpFileList
|
||||
[ssh]=sshFileList
|
||||
)
|
||||
[[ -n "${storageFunc[$storageType]:-}" ]] || { appError "Unknown value storageType: $storageType"; return 1; }
|
||||
|
||||
"${storageFunc[$storageType]}" "$@"
|
||||
}
|
||||
|
||||
# Cleans a specified directory on a remote server using rsync.
|
||||
#
|
||||
# $1 (path): The path to the directory to be cleaned on the remote server.
|
||||
#
|
||||
# The function creates a temporary empty directory on the local machine and syncs it with the remote directory
|
||||
# using rsync with the `--delete` option, which removes any files on the remote side that are not present in
|
||||
# the local directory. After completion, the temporary directory is deleted. An error message is displayed in case of failure.
|
||||
function rsyncDirectoryClean() {
|
||||
local path="$1"
|
||||
[[ -n "$path" ]] || { appError "Path not specified"; return 1; }
|
||||
|
||||
local emptyPath="$appDataPath/$(uuidgen)"
|
||||
local error
|
||||
runError error mkdir -p "$emptyPath" || { appError "Failed to create temp directory: $error"; return 1; }
|
||||
|
||||
runError error rsync -r --delete --password-file="$rsyncPassFile" "$emptyPath/" rsync://"$rsyncUri$path/" \
|
||||
|| { rm -rf "$emptyPath"; appError "Failed to clean remote directory: $error"; return 1; }
|
||||
rm -rf "$emptyPath"
|
||||
}
|
||||
|
||||
# Deletes a directory on an FTP server.
|
||||
#
|
||||
# $1 (path): The path to the directory on the FTP server to be deleted (in ftpPath).
|
||||
function ftpDirectoryDelete() {
|
||||
local path="$1"
|
||||
[[ -n "$path" ]] || { appError "Path not specified"; return 1; }
|
||||
|
||||
local error
|
||||
runError error curl -u "$ftpUser:$ftpPass" -Q "-RMD $ftpPath$path" "ftp://$ftpHost:$ftpPort" --connect-timeout 10 \
|
||||
|| { appError "$error"; return 1; }
|
||||
}
|
||||
|
||||
# Deletes a directory on a remote server via SSH (in sshPath).
|
||||
#
|
||||
# $1 (path): The path to the directory to be deleted.
|
||||
function sshDirectoryDelete() {
|
||||
local path="$1"
|
||||
[[ -n "$path" ]] || { appError "Path not specified"; return 1; }
|
||||
|
||||
local error
|
||||
runError error ssh -i "$sshKeyFile" "$sshUser@$sshHost" "rm -rf '${sshPath}${path}'" \
|
||||
|| { appError "$error"; return 1; }
|
||||
}
|
||||
|
||||
function fileAtomicWrite() {
|
||||
local file="$1"
|
||||
local content="$2"
|
||||
local path tmp
|
||||
path="$(dirname "$file")"
|
||||
|
||||
mkdir -p "$path"
|
||||
tmp="$(mktemp "$path/.tmp.XXXXXX")"
|
||||
printf '%s\n' "$content" > "$tmp"
|
||||
mv -f "$tmp" "$file"
|
||||
}
|
||||
|
||||
# Create a timestamped backup copy of a file if it exists.
|
||||
# $1 (file): path to the file to back up.
|
||||
# [$2] (suffix): custom suffix for the backup file (defaults to a timestamp .bak suffix).
|
||||
function fileBackup() {
|
||||
local file="$1"
|
||||
if [[ -z "$file" ]]; then
|
||||
appError "File not specified"
|
||||
return 1
|
||||
fi
|
||||
if [[ ! -f "$file" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local suffix="${2:-.$(date +%F_%H-%M-%S).bak}"
|
||||
|
||||
cp -p -- "$file" "$file$suffix" || {
|
||||
appError "Failed to backup file: $file"
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Download remote file to a local path.
|
||||
function fileDownload() {
|
||||
local remoteFile="$1"
|
||||
local localFile="$2"
|
||||
local retries="${3:-5}"
|
||||
local delay="${4:-3}"
|
||||
|
||||
[[ -n "$remoteFile" ]] || { appError "Remote file not specified"; return 1; }
|
||||
[[ -n "$localFile" ]] || { appError "Local file not specified"; return 1; }
|
||||
|
||||
mkdir -p "$(dirname "$localFile")" || { appError "Failed to create folder"; return 1; }
|
||||
|
||||
local tmpFile="${localFile}.part"
|
||||
local i rc curlError lastError
|
||||
for ((i = 1; i <= retries; i++)); do
|
||||
curlError=$(curl -kfsSL --http1.1 --max-redirs 10 --connect-timeout 30 --speed-time 60 --speed-limit 1024 -C - -o "$tmpFile" "$remoteFile" 2>&1)
|
||||
rc=$?
|
||||
if [[ $rc -eq 0 ]]; then
|
||||
mv -f "$tmpFile" "$localFile" || { appError "Failed to move downloaded file"; return 1; }
|
||||
return 0
|
||||
fi
|
||||
|
||||
lastError="$curlError"
|
||||
[[ $rc -ne 33 ]] || rm -f "$tmpFile" # rc=33: server doesn't support resume, restart from scratch
|
||||
|
||||
sleep "$delay"
|
||||
done
|
||||
|
||||
lastError="${lastError//$'\r'/ }"
|
||||
lastError="${lastError//$'\n'/ }"
|
||||
|
||||
appError "Failed to download file after $retries attempts | $lastError"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Creates an archive from a source file or directory.
|
||||
#
|
||||
# Supported archive formats are selected by the target file extension:
|
||||
# .zip, .tar.gz, or .tgz.
|
||||
#
|
||||
# $1 (source): Source file or directory to archive.
|
||||
# $2 (target): Target archive file path.
|
||||
#
|
||||
# Returns:
|
||||
# 0 on success, 1 on validation or archive creation failure.
|
||||
function archiveCreate() {
|
||||
local source="$1"
|
||||
if [[ -z "$source" ]]; then
|
||||
appError "Source path not specified"
|
||||
return 1
|
||||
fi
|
||||
if [[ ! -e "$source" ]]; then
|
||||
appError "Source path not found: $source"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local target="$2"
|
||||
if [[ -z "$target" ]]; then
|
||||
appError "Target file not specified"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local compressProgram="${3:-}"
|
||||
|
||||
local sourcePath sourceBase targetPath targetBase output rc
|
||||
sourcePath=$(dirname -- "$source")
|
||||
sourceBase=$(basename -- "$source")
|
||||
targetPath=$(dirname -- "$target")
|
||||
targetBase=$(basename -- "$target")
|
||||
|
||||
case "$targetBase" in
|
||||
*.zip|*.tar.gz|*.tgz)
|
||||
;;
|
||||
*)
|
||||
appError "Unknown type archive: $targetBase"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
mkdir -p -- "$targetPath" || {
|
||||
appError "Failed to create directory: $targetPath"
|
||||
return 1
|
||||
}
|
||||
|
||||
case "$targetBase" in
|
||||
*.zip)
|
||||
if [[ -d "$source" ]]; then
|
||||
output=$(cd "$source" && zip -qr "$target" . 2>&1)
|
||||
else
|
||||
output=$(cd "$sourcePath" && zip -qr "$target" "$sourceBase" 2>&1)
|
||||
fi
|
||||
rc=$?
|
||||
[[ $rc -le 1 ]] || { appError "Error creating ZIP (rc=$rc)${output:+: $output}"; return 1; }
|
||||
[[ $rc -ne 1 ]] || [[ -z "$output" ]] || appError "Warning creating ZIP${output:+: $output}"
|
||||
;;
|
||||
*.tar.gz|*.tgz)
|
||||
local -a tarCompressFlags
|
||||
if [[ -n "$compressProgram" ]]; then
|
||||
tarCompressFlags=("--use-compress-program=$compressProgram")
|
||||
else
|
||||
tarCompressFlags=("-z")
|
||||
fi
|
||||
if [[ -d "$source" ]]; then
|
||||
output=$(tar --warning=no-file-changed -c "${tarCompressFlags[@]}" -f "$target" -C "$source" . 2>&1)
|
||||
else
|
||||
output=$(tar --warning=no-file-changed -c "${tarCompressFlags[@]}" -f "$target" -C "$sourcePath" "$sourceBase" 2>&1)
|
||||
fi
|
||||
rc=$?
|
||||
[[ $rc -le 1 ]] || { appError "Error creating TAR (rc=$rc)${output:+: $output}"; return 1; }
|
||||
[[ $rc -ne 1 ]] || [[ -z "$output" ]] || appError "Warning creating TAR${output:+: $output}"
|
||||
;;
|
||||
esac
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Extracts an archive into a target directory.
|
||||
#
|
||||
# Supported archive formats are selected by the source file extension:
|
||||
# .zip, .tar.gz, or .tgz.
|
||||
#
|
||||
# $1 (source): Source archive file path.
|
||||
# $2 (target): Target directory where archive contents should be extracted.
|
||||
#
|
||||
# Returns:
|
||||
# 0 on success, 1 on validation or extraction failure.
|
||||
function archiveExtract() {
|
||||
local source="$1"
|
||||
if [[ -z "$source" ]]; then
|
||||
appError "Source archive not specified"
|
||||
return 1
|
||||
fi
|
||||
if [[ ! -f "$source" ]]; then
|
||||
appError "Source archive not found: $source"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local target="$2"
|
||||
if [[ -z "$target" ]]; then
|
||||
appError "Target directory not specified"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local sourceBase output rc
|
||||
sourceBase=$(basename -- "$source")
|
||||
|
||||
case "$sourceBase" in
|
||||
*.zip|*.tar.gz|*.tgz)
|
||||
;;
|
||||
*)
|
||||
appError "Unknown type archive: $sourceBase"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
mkdir -p -- "$target" || {
|
||||
appError "Failed to create directory: $target"
|
||||
return 1
|
||||
}
|
||||
|
||||
case "$sourceBase" in
|
||||
*.zip)
|
||||
output=$(unzip -oq "$source" -d "$target" 2>&1)
|
||||
rc=$?
|
||||
if [[ $rc -ne 0 ]]; then
|
||||
appError "Error extracting ZIP: $output"
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
*.tar.gz|*.tgz)
|
||||
output=$(tar -xzf "$source" -C "$target" 2>&1)
|
||||
rc=$?
|
||||
if [[ $rc -ne 0 ]]; then
|
||||
appError "Error extracting TAR: $output"
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Retrieves the size of the specified path (file or directory) in b/kb/mb/gb.
|
||||
#
|
||||
# $1 (path): The path to the file or directory whose size is to be retrieved.
|
||||
# $2 (unit): Unit.
|
||||
# $3 (precision): Precision.
|
||||
function pathSize() {
|
||||
local path="$1"
|
||||
local unit="${2:-}"
|
||||
local precision="${3:-0}"
|
||||
local divisor="1"
|
||||
case "${unit,,}" in
|
||||
kb) divisor="1024" ;;
|
||||
mb) divisor="1048576" ;;
|
||||
gb) divisor="1073741824" ;;
|
||||
esac
|
||||
[[ -n "$path" ]] || { appError "Path not specified"; return 1; }
|
||||
|
||||
local output error bytes
|
||||
if [[ -d "$path" ]]; then
|
||||
run output error du -sb "$path" || { appError "$error"; return 1; }
|
||||
bytes=$(printf '%s\n' "$output" | cut -f1)
|
||||
elif [[ -f "$path" ]]; then
|
||||
run output error stat -c %s "$path" || { appError "$error"; return 1; }
|
||||
bytes="$output"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
|
||||
LC_NUMERIC=C awk -v bytes="$bytes" -v divisor="$divisor" -v precision="$precision" 'BEGIN { printf "%.*f\n", precision, bytes / divisor }'
|
||||
}
|
||||
|
||||
function ftpPathSize() {
|
||||
local path="$1"
|
||||
local unit="${2:-}"
|
||||
local precision="${3:-0}"
|
||||
local divisor="1"
|
||||
case "${unit,,}" in
|
||||
kb) divisor="1024" ;;
|
||||
mb) divisor="1048576" ;;
|
||||
gb) divisor="1073741824" ;;
|
||||
esac
|
||||
|
||||
local output error total
|
||||
if run output error curl -sS -u "$ftpUser:$ftpPass" "ftp://$ftpHost:$ftpPort$ftpPath$path/" --connect-timeout 10; then
|
||||
total=0
|
||||
local line file size
|
||||
while IFS= read -r line; do
|
||||
file=$(awk '{print $NF}' <<< "$line")
|
||||
[[ -n "$file" && "$file" != "." && "$file" != ".." ]] || continue
|
||||
|
||||
if [[ "$line" =~ ^d ]]; then
|
||||
size=$(ftpPathSize "$path/$file") || return 1
|
||||
else
|
||||
size=$(awk '{print $5}' <<< "$line")
|
||||
[[ "$size" =~ ^[0-9]+$ ]] || continue
|
||||
fi
|
||||
(( total += size ))
|
||||
done <<< "$output"
|
||||
else
|
||||
local parent name
|
||||
parent=$(dirname "$path")
|
||||
name=$(basename "$path")
|
||||
run output error curl -sS -u "$ftpUser:$ftpPass" "ftp://$ftpHost:$ftpPort$ftpPath$parent/" --connect-timeout 10 || {
|
||||
appError "$error"; return 1
|
||||
}
|
||||
total=$(awk -v f="$name" '$NF == f {print $5}' <<< "$output")
|
||||
[[ "$total" =~ ^[0-9]+$ ]] || { appError "Cannot determine size of: $path"; return 1; }
|
||||
fi
|
||||
|
||||
LC_NUMERIC=C awk -v b="$total" -v d="$divisor" -v p="$precision" 'BEGIN { printf "%.*f\n", p, b/d }'
|
||||
}
|
||||
|
||||
# Return the contents of a file if it exists and is non-empty, otherwise write the given value to it and return it.
|
||||
# $1 (file): path to the file.
|
||||
# $2 (value): value to write when the file is missing or empty.
|
||||
function fileValueGetOrCreate() {
|
||||
local file="${1-}"
|
||||
local value="${2-}"
|
||||
|
||||
if [[ -z "$file" ]]; then
|
||||
appError "File not specified"
|
||||
return 1
|
||||
fi
|
||||
if [[ -s "$file" ]]; then
|
||||
cat "$file"
|
||||
return $?
|
||||
fi
|
||||
|
||||
if [[ -z "$value" ]]; then
|
||||
appError "Value not specified"
|
||||
return 1
|
||||
fi
|
||||
|
||||
mkdir -p -- "$(dirname -- "$file")" || {
|
||||
appError "Failed to create folder for: $file"
|
||||
return 1
|
||||
}
|
||||
|
||||
install -o root -g root -m 600 /dev/null "$file" || {
|
||||
appError "Failed to create file: $file"
|
||||
return 1
|
||||
}
|
||||
|
||||
printf '%s\n' "$value" > "$file" || {
|
||||
appError "Failed to save value: $file"
|
||||
return 1
|
||||
}
|
||||
|
||||
printf '%s\n' "$value"
|
||||
}
|
||||
|
||||
# Retrieves or generates a password and stores it in the specified file.
|
||||
#
|
||||
# $1: path to the file where the password should be stored.
|
||||
# [$2+]: additional parameters are passed to the stringRandom function for generating the password (optional).
|
||||
function filePasswordGetOrCreate() {
|
||||
local file="${1-}"
|
||||
if [[ -z "$file" ]]; then
|
||||
appError "File not specified"
|
||||
return 1
|
||||
fi
|
||||
shift || true
|
||||
|
||||
local value
|
||||
if [[ -s "$file" ]]; then
|
||||
cat "$file"
|
||||
return $?
|
||||
fi
|
||||
value="$(strRandom "$@")" || {
|
||||
appError "Failed to generate password"
|
||||
return 1
|
||||
}
|
||||
|
||||
fileValueGetOrCreate "$file" "$value"
|
||||
}
|
||||
|
||||
# Get value by key from "KEY=VALUE" content or file (returns first match)
|
||||
function configGet() {
|
||||
local file="${1-}"
|
||||
local key="${2-}"
|
||||
local line value
|
||||
|
||||
if [[ -z "$file" ]]; then
|
||||
appError "Config file not specified"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -z "$key" ]]; then
|
||||
appError "Config key not specified"
|
||||
return 1
|
||||
fi
|
||||
|
||||
[[ -f "$file" ]] || {
|
||||
printf '\n'
|
||||
return 0
|
||||
}
|
||||
|
||||
line="$(awk -F= -v key="$key" '$1 == key { print; exit }' "$file")" || {
|
||||
printf '\n';
|
||||
return 0;
|
||||
}
|
||||
[[ "$line" == "$key="* ]] || {
|
||||
printf '\n'
|
||||
return 0
|
||||
}
|
||||
|
||||
value="${line#*=}"
|
||||
value="$(sed -E 's/[[:space:]]+$//' <<<"$value")"
|
||||
|
||||
printf '%s\n' "$value"
|
||||
}
|
||||
|
||||
# Ensure key=value is present in file (remove previous key lines, then append).
|
||||
function configSet() {
|
||||
local file="${1-}"
|
||||
local key="${2-}"
|
||||
local value="${3-}"
|
||||
local tmp
|
||||
|
||||
if [[ -z "$file" ]]; then
|
||||
appError "Config file not specified"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -z "$key" ]]; then
|
||||
appError "Config key not specified"
|
||||
return 1
|
||||
fi
|
||||
|
||||
value="${value//$'\r'/ }"
|
||||
value="${value//$'\n'/ }"
|
||||
|
||||
mkdir -p -- "$(dirname -- "$file")" || {
|
||||
appError "Failed to create folder for: $file"
|
||||
return 1
|
||||
}
|
||||
|
||||
[[ -f "$file" ]] || install -o root -g root -m 600 /dev/null "$file" || {
|
||||
appError "Failed to create file: $file"
|
||||
return 1
|
||||
}
|
||||
|
||||
tmp="$(mktemp)" || return 1
|
||||
|
||||
awk -F= -v key="$key" '$1 != key' "$file" > "$tmp" || true
|
||||
|
||||
if [[ -n "$value" ]]; then
|
||||
printf '%s=%s\n' "$key" "$value" >> "$tmp" || {
|
||||
rm -f -- "$tmp"
|
||||
appError "Failed to write config value: $key"
|
||||
return 1
|
||||
}
|
||||
fi
|
||||
|
||||
cat "$tmp" > "$file" || {
|
||||
rm -f -- "$tmp"
|
||||
appError "Failed to update config file: $file"
|
||||
return 1
|
||||
}
|
||||
|
||||
rm -f -- "$tmp"
|
||||
}
|
||||
|
||||
# Get param from config or set via configSet if missing
|
||||
function configGetOrSet() {
|
||||
local file="${1-}"
|
||||
local key="${2-}"
|
||||
local value="${3-}"
|
||||
local current
|
||||
|
||||
current="$(configGet "$file" "$key")" || return 1
|
||||
if [[ -n "$current" ]]; then
|
||||
printf '%s\n' "$current"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -z "$value" ]]; then
|
||||
appError "Config value not specified: $key"
|
||||
return 1
|
||||
fi
|
||||
|
||||
configSet "$file" "$key" "$value" || return 1
|
||||
printf '%s\n' "$value"
|
||||
}
|
||||
|
||||
# Get param from config or create via configSet (+gen stringRandom if missing value) if missing
|
||||
function configGetOrCreate() {
|
||||
local file="${1-}"
|
||||
local key="${2-}"
|
||||
shift 2 || true
|
||||
|
||||
local value current
|
||||
|
||||
current="$(configGet "$file" "$key")" || return 1
|
||||
if [[ -n "$current" ]]; then
|
||||
printf '%s\n' "$current"
|
||||
return 0
|
||||
fi
|
||||
|
||||
value="$(strRandom "$@")" || {
|
||||
appError "Failed to generate config value: $key"
|
||||
return 1
|
||||
}
|
||||
|
||||
configSet "$file" "$key" "$value" || return 1
|
||||
printf '%s\n' "$value"
|
||||
}
|
||||
|
||||
# Check that no line in a file exceeds the specified maximum length, printing offending lines to stderr.
|
||||
# $1 (file): path to the file to check.
|
||||
# $2 (max): maximum allowed line length in characters.
|
||||
function fileCheckLineLength() {
|
||||
local file="$1"
|
||||
local max="$2"
|
||||
local line len num=0 found=0
|
||||
|
||||
[[ -f "$file" ]] || { appError "File not found: $file"; return 1; }
|
||||
[[ "$max" =~ ^[0-9]+$ ]] || { appError "Invalid max line length: $max"; return 1; }
|
||||
|
||||
while IFS= read -r line || [[ -n $line ]]; do
|
||||
((num++))
|
||||
len=${#line}
|
||||
|
||||
if (( len > max )); then
|
||||
printf 'Line %d exceeds %d characters (%d)\n' "$num" "$max" "$len" >&2
|
||||
found=1
|
||||
fi
|
||||
done < "$file"
|
||||
|
||||
return "$found"
|
||||
}
|
||||
|
||||
function fileSignatureDiffList() {
|
||||
local path="$1" type="$2" mask="$3"
|
||||
[[ -n "$path" && -n "$type" && -n "$mask" ]] || { appError "Missing argument(s)"; return 1; }
|
||||
[[ -d "$path" ]] || { appError "Path not found: $path"; return 1; }
|
||||
|
||||
find "$path" -type "$type" -printf '%m:%u:%g:%p\n' 2>/dev/null | grep -vE "$mask:"
|
||||
return 0
|
||||
}
|
||||
|
||||
function fileSignatureDiff() {
|
||||
local path="$1" mask="$2" sign
|
||||
[[ -n "$path" && -n "$mask" ]] || { appError "Missing argument(s)"; return 1; }
|
||||
[[ -d "$path" ]] || { appError "Path not found: $path"; return 1; }
|
||||
|
||||
sign="$(stat -c '%a:%U:%G' "$path")"
|
||||
[[ "$sign" == "$mask" ]] || printf "%s\n" "$sign:$path";
|
||||
}
|
||||
|
||||
function fileSignatureAclDiff() {
|
||||
local path="$1" mask="$2"
|
||||
[[ -n "$path" && -n "$mask" ]] || { printDanger "Missing argument(s)"; return 1; }
|
||||
[[ -d "$path" ]] || { printDanger "Path not found: $path"; return 1; }
|
||||
|
||||
local acl unexpected
|
||||
acl=$(getfacl -p "$path" 2>/dev/null)
|
||||
unexpected=$(grep -vE "^(#|\$|(default:)?((user|group|mask)::|other::---|$mask\$))" <<< "$acl" | paste -sd '|')
|
||||
|
||||
grep -qxF "$mask" <<< "$acl" || unexpected+="${unexpected:+|}missing:$mask"
|
||||
grep -qxF "default:$mask" <<< "$acl" || unexpected+="${unexpected:+|}missing:default:$mask"
|
||||
|
||||
[[ -z "$unexpected" ]] || printf "%s\n" "$unexpected:$path"
|
||||
}
|
||||
|
||||
function fileSignatureCheck() {
|
||||
local output error
|
||||
run output error fileSignatureDiff "$@" || return 1
|
||||
[[ -z "$output" ]]
|
||||
}
|
||||
|
||||
function fileSignatureAclCheck() {
|
||||
local output error
|
||||
run output error fileSignatureAclDiff "$@" || return 1
|
||||
[[ -z "$output" ]]
|
||||
}
|
||||
|
||||
# Checking the availability of a file (URL) for download.
|
||||
# Check if a URL is accessible and print the final effective URL on success.
|
||||
# $1 (url): the URL to check.
|
||||
function urlAccessible() {
|
||||
local url="$1" code final
|
||||
if [[ -z "$url" ]]; then
|
||||
appError "URL not specified"
|
||||
return 1
|
||||
fi
|
||||
|
||||
final=$(curl -ksSL --max-redirs 10 --range 0-0 -o /dev/null -w "%{url_effective} %{http_code}" "$url") || return 1
|
||||
code="${final##* }" # http code
|
||||
final="${final% *}" # final URL
|
||||
if [[ "$code" =~ ^[23][0-9]{2}$ ]]; then
|
||||
printf "%s" "$final"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Fetch and print the HTTP status code for the given URL.
|
||||
# $1 (url): the URL to request.
|
||||
function urlCode() {
|
||||
local url="$1" code
|
||||
if [[ -z "$url" ]]; then
|
||||
appError "URL not specified"
|
||||
return 1
|
||||
fi
|
||||
|
||||
code=$(curl -ksSL --max-redirs 10 -o /dev/null -w "%{http_code}" "$url") || return 1
|
||||
printf "%s" "$code"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Derive a local filename from a URL, optionally using a custom base name.
|
||||
# $1 (url): the source URL to extract the filename from.
|
||||
# [$2] (localFileName): custom base name; if given, the URL extension is appended to it.
|
||||
function urlLocalFileGen() {
|
||||
local url="$1" code
|
||||
if [[ -z "$url" ]]; then
|
||||
appError "URL not specified"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local localFileName="$2"
|
||||
|
||||
name="${url##*/}"
|
||||
name="${name%%\?*}"
|
||||
base="${name%%.*}"
|
||||
ext="${name#*.}"
|
||||
|
||||
if [[ -z "$localFileName" ]]; then
|
||||
printf '%s' "$name"
|
||||
else
|
||||
printf '%s' "$localFileName.$ext"
|
||||
fi
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
# Remove a single trailing slash from a string.
|
||||
# $1 (s): input string.
|
||||
function strTrimSlash() {
|
||||
local s="${1-}"
|
||||
s="${s%/}"
|
||||
printf '%s' "$s"
|
||||
}
|
||||
|
||||
# Generates a random password with a specified length and character set.
|
||||
#
|
||||
# $1 (length): length of the password (defaults to 32 characters).
|
||||
# $2 (charset): string of characters to use for generating the password (defaults to "A-Za-z0-9@#$%^*_+=[]{}:").
|
||||
function strRandom() {
|
||||
local length="${1:-32}"
|
||||
local charset="${2:-"A-Za-z0-9_.-"}"
|
||||
LC_ALL=C tr -dc "$charset" < /dev/urandom | head -c "$length"
|
||||
}
|
||||
|
||||
# Format a number with thousands separators (space-separated groups).
|
||||
# $1 (number): the number to format.
|
||||
function numFormat() {
|
||||
printf '%s\n' "${1-}" | sed ':a;s/\B[0-9]\{3\}\>/ &/;ta'
|
||||
}
|
||||
|
||||
# Check if a value exists in an array passed as remaining arguments.
|
||||
# $1 (needle): value to search for.
|
||||
# $2 (items): array elements to search in (passed as individual arguments).
|
||||
function arrayContains() {
|
||||
local needle="${1-}"
|
||||
shift || true
|
||||
|
||||
local item
|
||||
for item in "$@"; do
|
||||
[[ "$item" == "$needle" ]] && return 0
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Return success if $value exists as full line in multiline $list
|
||||
function listContains() {
|
||||
local value="${1-}"
|
||||
local list="${2-}"
|
||||
|
||||
[[ -n "$value" ]] || return 1
|
||||
|
||||
grep -Fxq -- "$value" <<<"$list"
|
||||
}
|
||||
|
||||
# Calculate the difference in seconds between two datetime strings.
|
||||
# $1 (from): start datetime string (accepted by date -d).
|
||||
# $2 (to): end datetime string (accepted by date -d).
|
||||
function timeDiff() {
|
||||
local from="${1-}"
|
||||
local to="${2-}"
|
||||
local t1 t2
|
||||
|
||||
[[ -n "$from" ]] || { appError "time_from not specified"; return 1; }
|
||||
[[ -n "$to" ]] || { appError "time_to not specified"; return 1; }
|
||||
|
||||
t1="$(date -d "$from" +%s)" || { appError "invalid time_from: $from"; return 1; }
|
||||
t2="$(date -d "$to" +%s)" || { appError "invalid time_to: $to"; return 1; }
|
||||
|
||||
printf '%s\n' "$(( t2 - t1 ))"
|
||||
}
|
||||
|
||||
# Normalize a domain name: lowercase, strip whitespace and trailing dot, then IDN-encode.
|
||||
# $1 (domain): raw domain string to normalize.
|
||||
function domainPrepare() {
|
||||
local domain="${1-}"
|
||||
|
||||
domain="${domain,,}"
|
||||
domain="${domain//[[:space:]]/}"
|
||||
domain="${domain%.}"
|
||||
|
||||
LC_ALL=C.UTF-8 idn2 <<<"$domain"
|
||||
}
|
||||
|
||||
# Checks if the given string is a valid domain.
|
||||
#
|
||||
# $1 (domain): a string representing the domain.
|
||||
function domainValidate() {
|
||||
local domain="${1-}"
|
||||
|
||||
(( ${#domain} >= 4 )) || { appError "Domain name is too short: $domain"; return 1; }
|
||||
(( ${#domain} <= 253 )) || { appError "Domain name is too long: $domain"; return 1; }
|
||||
[[ "$domain" =~ ^([A-Za-z0-9]([-A-Za-z0-9]{0,61}[A-Za-z0-9])?\.)+([A-Za-z]{2,63}|xn--[A-Za-z0-9-]{2,59})$ ]] || { appError "Invalid domain name format: $domain"; return 1; }
|
||||
}
|
||||
|
||||
# Validate a domain name and reject reserved names.
|
||||
# $1 (domain): domain name to check.
|
||||
function domainCheck() {
|
||||
local domain="${1-}"
|
||||
local reserved=("root" "user")
|
||||
|
||||
arrayContains "$domain" "${reserved[@]}" && { appError "Reserved domain name: $domain"; return 1; }
|
||||
domainValidate "$domain"
|
||||
}
|
||||
|
||||
# Generate username and groupname from domain
|
||||
function domainToUser() {
|
||||
local domain="${1-}"
|
||||
local base hash
|
||||
|
||||
base="$(printf '%s' "$domain" \
|
||||
| tr '[:upper:]' '[:lower:]' \
|
||||
| sed -E 's/[^a-z0-9-]+/-/g; s/-{2,}/-/g; s/^-//; s/-$//')"
|
||||
|
||||
hash="$(printf '%s' "$domain$hostSalt" \
|
||||
| sha256sum \
|
||||
| awk '{print substr($1,1,8)}')"
|
||||
|
||||
printf '%s' "${base:0:21}-${hash}"
|
||||
}
|
||||
|
||||
function domainToUid() {
|
||||
local domain="${1-}"
|
||||
local username uid
|
||||
|
||||
if [[ -z "$domain" ]]; then
|
||||
appError "Domain not specified"
|
||||
return 1
|
||||
fi
|
||||
|
||||
username=$(domainToUser "$domain")
|
||||
if [[ -z "$username" ]]; then
|
||||
appError "Cannot compute username for: $domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
uid=$(id -u "$username" 2>/dev/null)
|
||||
if [[ -z "$uid" ]]; then
|
||||
appError "No such user: $username"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ "$uid" == "0" ]]; then
|
||||
appError "Refusing to return root UID for: $domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf '%s' "$uid"
|
||||
}
|
||||
|
||||
function domainToGid() {
|
||||
local domain="${1-}"
|
||||
local username gid
|
||||
|
||||
if [[ -z "$domain" ]]; then
|
||||
appError "Domain not specified"
|
||||
return 1
|
||||
fi
|
||||
|
||||
username=$(domainToUser "$domain")
|
||||
if [[ -z "$username" ]]; then
|
||||
appError "Cannot compute username for: $domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
gid=$(id -g "$username" 2>/dev/null)
|
||||
if [[ -z "$gid" ]]; then
|
||||
appError "No such user: $username"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ "$gid" == "0" ]]; then
|
||||
appError "Refusing to return root GID for: $domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf '%s' "$gid"
|
||||
}
|
||||
|
||||
# Generate random string from domain
|
||||
function domainToRandom() {
|
||||
local domain="${1-}"
|
||||
local maxLen="${2:-256}"
|
||||
local tailLen="${3:-8}"
|
||||
local base tail baseLen
|
||||
|
||||
base="$(printf '%s' "$domain" \
|
||||
| tr '[:upper:]' '[:lower:]' \
|
||||
| sed -E 's/[^a-z0-9]+/_/g; s/_{2,}/_/g; s/^_//; s/_$//')"
|
||||
|
||||
tail="$(strRandom "$tailLen" 'a-z0-9')" || return 1
|
||||
|
||||
baseLen=$(( maxLen - tailLen - 1 ))
|
||||
(( baseLen < 1 )) && baseLen=1
|
||||
|
||||
printf '%s' "${base:0:baseLen}_${tail}"
|
||||
}
|
||||
|
||||
# Run a command and capture its stdout and stderr into named variables.
|
||||
# $1 (outVar): name of the variable to receive stdout.
|
||||
# $2 (errVar): name of the variable to receive stderr.
|
||||
# $3 (cmd): command and arguments to execute.
|
||||
function run() {
|
||||
local -n __runOut="$1"
|
||||
local -n __runError="$2"
|
||||
shift 2
|
||||
|
||||
local stdoutTmp stderrTmp status
|
||||
stdoutTmp=$(mktemp) || return 1
|
||||
stderrTmp=$(mktemp) || { rm -f "$stdoutTmp"; return 1; }
|
||||
|
||||
"$@" >"$stdoutTmp" 2>"$stderrTmp"
|
||||
status=$?
|
||||
|
||||
__runOut=$(<"$stdoutTmp")
|
||||
__runError=$(<"$stderrTmp")
|
||||
|
||||
rm -f "$stdoutTmp" "$stderrTmp"
|
||||
return "$status"
|
||||
}
|
||||
|
||||
# Run command, discard stdout
|
||||
function runError() {
|
||||
local -n __runErrorErr="$1"
|
||||
shift || true
|
||||
|
||||
local __runErrorOutput __runErrorError status
|
||||
run __runErrorOutput __runErrorError "$@"
|
||||
status=$?
|
||||
|
||||
if [[ -n "$__runErrorError" ]]; then
|
||||
__runErrorErr="$__runErrorError"
|
||||
else
|
||||
__runErrorErr="$__runErrorOutput"
|
||||
fi
|
||||
|
||||
return "$status"
|
||||
}
|
||||
|
||||
# Run a command, discarding output, and print an error message on failure.
|
||||
# $1 (cmd): command and arguments to execute.
|
||||
function runFail() {
|
||||
local __runFailErr
|
||||
runError __runFailErr "$@"
|
||||
local status=$?
|
||||
[[ $status -eq 0 ]] || appError "$__runFailErr"
|
||||
return $status
|
||||
}
|
||||
|
||||
# Run a command and discard all stdout and stderr output.
|
||||
# $1 (cmd): command and arguments to execute.
|
||||
function runSilent() {
|
||||
local output error
|
||||
run output error "$@"
|
||||
return $?
|
||||
}
|
||||
|
||||
# Run a shell command string (with operator support) and capture stdout and stderr into named variables.
|
||||
# $1 (outVar): name of the variable to receive stdout.
|
||||
# $2 (errVar): name of the variable to receive stderr.
|
||||
# $3 (cmd): command and arguments, including shell operators (|, &&, ;, etc.).
|
||||
function runShell() {
|
||||
local -n __out="$1"
|
||||
local -n __err="$2"
|
||||
shift 2
|
||||
|
||||
local stdoutTmp stderrTmp status
|
||||
stdoutTmp=$(mktemp) || return 1
|
||||
stderrTmp=$(mktemp) || { rm -f "$stdoutTmp"; return 1; }
|
||||
|
||||
# Arguments: > >> < | || & && ; ( ) convert to operators.
|
||||
local cmd="" arg
|
||||
for arg in "$@"; do
|
||||
if [[ "$arg" =~ ^([0-9]?>|[0-9]?>>|[0-9]?<|\||\|\||&&|;|\(|\))$ ]]; then
|
||||
cmd+=" $arg"
|
||||
else
|
||||
cmd+=" $(printf '%q' "$arg")"
|
||||
fi
|
||||
done
|
||||
|
||||
(
|
||||
set -o pipefail
|
||||
eval -- "$cmd"
|
||||
) >"$stdoutTmp" 2>"$stderrTmp"
|
||||
status=$?
|
||||
|
||||
__out=$(<"$stdoutTmp")
|
||||
__err=$(<"$stderrTmp")
|
||||
|
||||
rm -f "$stdoutTmp" "$stderrTmp"
|
||||
return "$status"
|
||||
}
|
||||
|
||||
# Converts notation (28Gi, 512Mi, 1Ki, 4Gb, ...) to bytes; returns -1 for empty input.
|
||||
function sizeToBytes() {
|
||||
local v="$1"
|
||||
case "$v" in
|
||||
*Ti) echo $(( ${v%Ti} * 1099511627776 )) ;;
|
||||
*Gi) echo $(( ${v%Gi} * 1073741824 )) ;;
|
||||
*Mi) echo $(( ${v%Mi} * 1048576 )) ;;
|
||||
*Ki) echo $(( ${v%Ki} * 1024 )) ;;
|
||||
*Tb) echo $(( ${v%Tb} * 1000000000000 )) ;;
|
||||
*Gb) echo $(( ${v%Gb} * 1000000000 )) ;;
|
||||
*Mb) echo $(( ${v%Mb} * 1000000 )) ;;
|
||||
*Kb) echo $(( ${v%Kb} * 1000 )) ;;
|
||||
"") echo -1 ;;
|
||||
*) echo "$v" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
#=========================================================================
|
||||
|
||||
# Comment out the first non-empty, non-commented line in the domains list file.
|
||||
# Prints the domain name on success, returns 1 if no domain available.
|
||||
function domainsListMark() {
|
||||
local file="$1"
|
||||
local line domain num=0 lineNum=0
|
||||
|
||||
[[ -f "$file" ]] || { appError "Domains list file not found: $file"; return 1; }
|
||||
while IFS= read -r line; do
|
||||
(( num++ ))
|
||||
[[ -z "$line" || "$line" == \#* ]] && continue
|
||||
domain="$line"
|
||||
lineNum=$num
|
||||
break
|
||||
done < "$file"
|
||||
|
||||
[[ $lineNum -eq 0 ]] && return 1
|
||||
|
||||
sed -i "${lineNum}s/^/#/" "$file"
|
||||
printf '%s\n' "$domain"
|
||||
}
|
||||
|
||||
# Remove the '#' prefix from the specified domain line in the domains list file.
|
||||
function domainsListUnmark() {
|
||||
local file="$1"
|
||||
local domain="$2"
|
||||
local line num=0 lineNum=0
|
||||
|
||||
[[ -f "$file" ]] || { appError "Domains list file not found: $file"; return 1; }
|
||||
[[ -n "$domain" ]] || { appError "Domain not specified"; return 1; }
|
||||
while IFS= read -r line; do
|
||||
(( num++ ))
|
||||
[[ "$line" == "#${domain}" ]] && { lineNum=$num; break; }
|
||||
done < "$file"
|
||||
|
||||
[[ $lineNum -eq 0 ]] && { appError "Domain not marked in list: $domain"; return 1; }
|
||||
|
||||
sed -i "${lineNum}s/^#//" "$file"
|
||||
}
|
||||
|
||||
|
||||
# Sends an email with the specified subject and body.
|
||||
#
|
||||
# $1 (mailSubject): string containing the subject of the email.
|
||||
# $2 (mailBody): string containing the body of the email.
|
||||
function emailSend() {
|
||||
local mailSubject="$1"
|
||||
local mailBody="$2"
|
||||
|
||||
[[ -n "$mailSubject" ]] || { appError "Subject not specified"; return 1; }
|
||||
[[ -n "$mailBody" ]] || { appError "Body email not specified"; return 1; }
|
||||
|
||||
local result
|
||||
result=$(printf 'Subject:%s\nFrom:%s\nTo:%s\n\n%s' "$mailSubject" "$mailFrom" "$mailTo" "$mailBody" | msmtp "$mailTo" 2>&1)
|
||||
[[ $? -eq 0 ]] || { appError "$result"; return 1; }
|
||||
}
|
||||
@@ -0,0 +1,581 @@
|
||||
#!/usr/bin/env perl
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
my ($file, $mode, @args) = @ARGV;
|
||||
|
||||
defined $file && length $file or die "usage: $0 <file> <mode> ...\n";
|
||||
defined $mode && length $mode or die "mode is required\n";
|
||||
|
||||
sub mask_to_re {
|
||||
my ($mask) = @_;
|
||||
return if !defined($mask) || $mask eq '';
|
||||
$mask = quotemeta($mask);
|
||||
$mask =~ s/\\\*/.*/g;
|
||||
return qr/\A$mask\z/;
|
||||
}
|
||||
|
||||
sub parse_kv_line {
|
||||
my ($line, $wanted_key) = @_;
|
||||
return unless $line =~ /^(\h*)\Q$wanted_key\E(?:\h+(.*?))?\h*(?:\R)?$/;
|
||||
my $indent = $1;
|
||||
my $value = defined($2) ? $2 : '';
|
||||
return ($indent, $value);
|
||||
}
|
||||
|
||||
sub line_matches_delete {
|
||||
my ($line, $wanted_key, $value_re) = @_;
|
||||
my @m = parse_kv_line($line, $wanted_key) or return 0;
|
||||
return 1 unless $value_re;
|
||||
return $m[1] =~ $value_re;
|
||||
}
|
||||
|
||||
sub line_matches_exact {
|
||||
my ($line, $wanted_key, $wanted_value) = @_;
|
||||
my @m = parse_kv_line($line, $wanted_key) or return 0;
|
||||
return $m[1] eq $wanted_value;
|
||||
}
|
||||
|
||||
sub fmt_line {
|
||||
my ($indent, $k, $v) = @_;
|
||||
return sprintf("%s%-23s %s\n", $indent, $k, $v);
|
||||
}
|
||||
|
||||
sub brace_delta {
|
||||
my ($line) = @_;
|
||||
return ($line =~ tr/\{//) - ($line =~ tr/\}//);
|
||||
}
|
||||
|
||||
sub key_add {
|
||||
my ($lines, $section_re, $key, $value) = @_;
|
||||
# die "value is required for add\n" if !defined($value) || $value eq '';
|
||||
|
||||
if ($section_re eq '') {
|
||||
my $depth = 0;
|
||||
|
||||
for my $line (@$lines) {
|
||||
if ($depth == 0 && line_matches_exact($line, $key, $value)) {
|
||||
return;
|
||||
}
|
||||
$depth += brace_delta($line);
|
||||
}
|
||||
|
||||
my $new = fmt_line('', $key, $value);
|
||||
|
||||
my $root_pos;
|
||||
my $first_block_pos;
|
||||
$depth = 0;
|
||||
|
||||
for (my $i = 0; $i <= $#$lines; $i++) {
|
||||
my $line = $lines->[$i];
|
||||
|
||||
if ($depth == 0) {
|
||||
$root_pos = $i
|
||||
if !defined($root_pos) && $line =~ /^\h*serverName\h+\S*/;
|
||||
|
||||
$first_block_pos = $i
|
||||
if !defined($first_block_pos) && $line =~ /^\h*\S.*\{\h*(?:\R)?$/;
|
||||
}
|
||||
|
||||
$depth += brace_delta($line);
|
||||
}
|
||||
|
||||
if (defined $root_pos) {
|
||||
splice @$lines, $root_pos + 1, 0, $new;
|
||||
} elsif (defined $first_block_pos) {
|
||||
splice @$lines, $first_block_pos, 0, $new;
|
||||
} else {
|
||||
push @$lines, $new;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
my ($start, $end, $indent) = block_find($lines, $section_re);
|
||||
|
||||
my $depth = 1;
|
||||
for my $i ($start + 1 .. $end - 1) {
|
||||
my $line = $lines->[$i];
|
||||
if ($depth == 1 && line_matches_exact($line, $key, $value)) {
|
||||
return;
|
||||
}
|
||||
$depth += brace_delta($line);
|
||||
}
|
||||
|
||||
my $new = fmt_line($indent, $key, $value);
|
||||
splice @$lines, $end, 0, $new;
|
||||
}
|
||||
|
||||
sub key_del {
|
||||
my ($lines, $section_re, $key, $mask) = @_;
|
||||
my $value_re = mask_to_re($mask);
|
||||
|
||||
if ($section_re eq '') {
|
||||
my @out;
|
||||
my $depth = 0;
|
||||
|
||||
for my $line (@$lines) {
|
||||
my $remove = ($depth == 0 && line_matches_delete($line, $key, $value_re)) ? 1 : 0;
|
||||
push @out, $line unless $remove;
|
||||
$depth += brace_delta($line);
|
||||
}
|
||||
|
||||
@$lines = @out;
|
||||
return;
|
||||
}
|
||||
|
||||
my ($start, $end, undef) = block_find($lines, $section_re);
|
||||
|
||||
my @out;
|
||||
push @out, @$lines[0 .. $start];
|
||||
|
||||
my $depth = 1;
|
||||
for my $i ($start + 1 .. $end - 1) {
|
||||
my $line = $lines->[$i];
|
||||
my $remove = ($depth == 1 && line_matches_delete($line, $key, $value_re)) ? 1 : 0;
|
||||
push @out, $line unless $remove;
|
||||
$depth += brace_delta($line);
|
||||
}
|
||||
|
||||
push @out, @$lines[$end .. $#$lines];
|
||||
@$lines = @out;
|
||||
}
|
||||
|
||||
sub block_find {
|
||||
my ($lines, $re) = @_;
|
||||
|
||||
for (my $i = 0; $i <= $#$lines; $i++) {
|
||||
next unless $lines->[$i] =~ /^(\h*)$re\h*\{\h*(?:\R)?$/;
|
||||
|
||||
my $indent = $1 . ' ';
|
||||
my $depth = 1;
|
||||
|
||||
for (my $j = $i + 1; $j <= $#$lines; $j++) {
|
||||
$depth += brace_delta($lines->[$j]);
|
||||
if ($depth == 0) {
|
||||
return ($i, $j, $indent);
|
||||
}
|
||||
}
|
||||
|
||||
die "block '$re' is not closed\n";
|
||||
}
|
||||
|
||||
die "block '$re' not found\n";
|
||||
}
|
||||
|
||||
sub block_add {
|
||||
my ($lines, $header) = @_;
|
||||
return if !defined($header) || $header eq '';
|
||||
|
||||
for my $line (@$lines) {
|
||||
return if $line =~ /^\h*\Q$header\E\h*\{/;
|
||||
}
|
||||
|
||||
if (@$lines && $lines->[-1] !~ /\n\z/) {
|
||||
$lines->[-1] .= "\n";
|
||||
}
|
||||
push @$lines, "\n" if @$lines && $lines->[-1] !~ /^\s*$/;
|
||||
push @$lines, "$header {\n";
|
||||
push @$lines, "}\n";
|
||||
}
|
||||
|
||||
sub block_del {
|
||||
my ($lines, $header_re) = @_;
|
||||
|
||||
my @out;
|
||||
my $deleted = 0;
|
||||
|
||||
for (my $i = 0; $i <= $#$lines; $i++) {
|
||||
my $line = $lines->[$i];
|
||||
|
||||
if ($line =~ /^\h*$header_re\h*\{\h*(?:\R)?$/) {
|
||||
my $depth = 1;
|
||||
$deleted = 1;
|
||||
|
||||
for ($i = $i + 1; $i <= $#$lines; $i++) {
|
||||
$depth += brace_delta($lines->[$i]);
|
||||
last if $depth == 0;
|
||||
}
|
||||
|
||||
next;
|
||||
}
|
||||
|
||||
push @out, $line;
|
||||
}
|
||||
|
||||
@$lines = @out;
|
||||
return $deleted;
|
||||
}
|
||||
|
||||
sub suspended_rebuild {
|
||||
my ($lines, @list) = @_;
|
||||
|
||||
@list = grep { defined($_) && $_ ne '' } @list;
|
||||
|
||||
key_del($lines, '', 'suspendedVhosts', '');
|
||||
|
||||
if (@list) {
|
||||
key_add($lines, '', 'suspendedVhosts', join(',', @list));
|
||||
}
|
||||
}
|
||||
|
||||
sub suspended_list {
|
||||
my ($lines) = @_;
|
||||
|
||||
my $depth = 0;
|
||||
for my $line (@$lines) {
|
||||
if ($depth == 0) {
|
||||
my @m = parse_kv_line($line, 'suspendedVhosts');
|
||||
if (@m) {
|
||||
return grep { length }
|
||||
map { s/^\h+|\h+$//gr }
|
||||
split /,/, $m[1];
|
||||
}
|
||||
}
|
||||
$depth += brace_delta($line);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
sub suspended_add {
|
||||
my ($lines, $domain) = @_;
|
||||
return if !defined($domain) || $domain eq '';
|
||||
|
||||
my @current = suspended_list($lines);
|
||||
return if grep { $_ eq $domain } @current;
|
||||
suspended_rebuild($lines, @current, $domain);
|
||||
}
|
||||
|
||||
sub suspended_del {
|
||||
my ($lines, $domain) = @_;
|
||||
return if !defined($domain) || $domain eq '';
|
||||
|
||||
suspended_rebuild($lines, grep { $_ ne $domain } suspended_list($lines));
|
||||
}
|
||||
|
||||
sub vhost_list {
|
||||
my ($lines, $type) = @_;
|
||||
$type //= 'all';
|
||||
|
||||
die "unknown vhost list type '$type'\n"
|
||||
if $type !~ /\A(?:all|up|down)\z/;
|
||||
|
||||
my @all;
|
||||
my %down = map { $_ => 1 } suspended_list($lines);
|
||||
|
||||
for my $line (@$lines) {
|
||||
my $clean = $line;
|
||||
$clean =~ s/#.*$//;
|
||||
$clean =~ s/^\s+|\s+$//g;
|
||||
next if $clean eq '';
|
||||
|
||||
if ($clean =~ /^virtualhost\s+([^\s\{]+)\s*\{?/i) {
|
||||
push @all, $1;
|
||||
}
|
||||
}
|
||||
|
||||
if ($type eq 'all') {
|
||||
print "$_\n" for @all;
|
||||
} elsif ($type eq 'down') {
|
||||
print "$_\n" for grep { exists $down{$_} } @all;
|
||||
} elsif ($type eq 'up') {
|
||||
print "$_\n" for grep { !exists $down{$_} } @all;
|
||||
}
|
||||
}
|
||||
|
||||
sub vhost_del {
|
||||
my ($lines, $domain) = @_;
|
||||
|
||||
key_del($lines, 'listener\h+HTTP', 'map', "$domain *");
|
||||
block_del($lines, 'virtualhost\h+' . quotemeta($domain));
|
||||
}
|
||||
|
||||
sub vhost_set {
|
||||
my ($lines, $vhRoot, $domain, @aliases) = @_;
|
||||
|
||||
vhost_del($lines, $domain);
|
||||
block_add($lines, "virtualhost $domain");
|
||||
|
||||
my $block_re = 'virtualhost\h+' . quotemeta($domain);
|
||||
key_add($lines, $block_re, 'vhRoot', $vhRoot);
|
||||
key_add($lines, $block_re, 'configFile', "/usr/local/lsws/conf/vhosts/$domain.conf");
|
||||
key_add($lines, $block_re, 'allowSymbolLink', '1');
|
||||
key_add($lines, $block_re, 'enableScript', '1');
|
||||
key_add($lines, $block_re, 'restrained', '1');
|
||||
key_add($lines, $block_re, 'setUIDMode', '2');
|
||||
|
||||
key_add($lines, 'listener\h+HTTP', 'map', "$domain $domain");
|
||||
for my $alias (@aliases) {
|
||||
key_add($lines, 'listener\h+HTTP', 'map', "$domain $alias");
|
||||
}
|
||||
}
|
||||
|
||||
sub vhost_alias {
|
||||
my ($lines, $name) = @_;
|
||||
|
||||
die "virtual host name is required\n"
|
||||
if !defined($name) || $name eq '';
|
||||
|
||||
my ($start, $end) = block_find($lines, 'listener\h+HTTP');
|
||||
|
||||
my %seen;
|
||||
|
||||
for my $i ($start + 1 .. $end - 1) {
|
||||
my $clean = $lines->[$i];
|
||||
$clean =~ s/#.*$//;
|
||||
$clean =~ s/^\s+|\s+$//g;
|
||||
next if $clean eq '';
|
||||
next unless $clean =~ /^map\s+(\S+)\s+(\S+)$/i;
|
||||
|
||||
my ($vhost, $domain) = ($1, $2);
|
||||
|
||||
next if $name ne 'all' && $vhost ne $name;
|
||||
next if $domain eq $vhost;
|
||||
next if $seen{$domain}++;
|
||||
|
||||
print "$domain\n";
|
||||
}
|
||||
}
|
||||
|
||||
sub member_list {
|
||||
my ($lines, $template, $type) = @_;
|
||||
|
||||
my ($t_start, $t_end) = block_find($lines, 'vhTemplate\h+' . quotemeta($template));
|
||||
|
||||
die "unknown member list type '$type'\n"
|
||||
if $type !~ /\A(?:all|up|down)\z/;
|
||||
|
||||
my %down = map { $_ => 1 } suspended_list($lines);
|
||||
|
||||
my $depth = 0;
|
||||
for my $i ($t_start + 1 .. $t_end - 1) {
|
||||
my $line = $lines->[$i];
|
||||
|
||||
if ($depth == 0 && $line =~ /^\h*member\h+([^\s\{]+)/i) {
|
||||
my $name = $1;
|
||||
if ( $type eq 'all'
|
||||
|| ($type eq 'down' && exists $down{$name})
|
||||
|| ($type eq 'up' && !exists $down{$name})) {
|
||||
print "$name\n";
|
||||
}
|
||||
}
|
||||
|
||||
$depth += brace_delta($line);
|
||||
}
|
||||
}
|
||||
|
||||
sub member_del {
|
||||
my ($lines, $template, $domain) = @_;
|
||||
|
||||
my ($t_start, $t_end) = block_find($lines, 'vhTemplate\h+' . quotemeta($template));
|
||||
|
||||
my $depth = 0;
|
||||
for my $i ($t_start + 1 .. $t_end - 1) {
|
||||
my $line = $lines->[$i];
|
||||
|
||||
if ($depth == 0 && $line =~ /^\h*member\h+\Q$domain\E\h*(\{)?\h*(?:\R)?$/) {
|
||||
my $m_end = $i;
|
||||
|
||||
if (defined $1) {
|
||||
my $d = 1;
|
||||
for my $j ($i + 1 .. $t_end - 1) {
|
||||
$d += brace_delta($lines->[$j]);
|
||||
if ($d == 0) { $m_end = $j; last; }
|
||||
}
|
||||
die "member '$domain' block is not closed\n" if $m_end == $i;
|
||||
}
|
||||
|
||||
splice @$lines, $i, $m_end - $i + 1;
|
||||
return;
|
||||
}
|
||||
|
||||
$depth += brace_delta($line);
|
||||
}
|
||||
}
|
||||
|
||||
sub member_set {
|
||||
my ($lines, $template, $domain, @aliases) = @_;
|
||||
|
||||
member_del($lines, $template, $domain);
|
||||
|
||||
my ($t_start, $t_end, $indent) = block_find($lines, 'vhTemplate\h+' . quotemeta($template));
|
||||
|
||||
my @new;
|
||||
if (@aliases) {
|
||||
push @new, fmt_line($indent, 'member', "$domain {");
|
||||
push @new, fmt_line($indent . ' ', 'vhAliases', join(', ', @aliases));
|
||||
push @new, "$indent}\n";
|
||||
} else {
|
||||
push @new, fmt_line($indent, 'member', $domain);
|
||||
}
|
||||
|
||||
splice @$lines, $t_end, 0, @new;
|
||||
}
|
||||
|
||||
sub member_alias {
|
||||
my ($lines, $template, $domain) = @_;
|
||||
|
||||
my ($t_start, $t_end) = block_find($lines, 'vhTemplate\h+' . quotemeta($template));
|
||||
|
||||
my $depth = 0;
|
||||
for my $i ($t_start + 1 .. $t_end - 1) {
|
||||
my $line = $lines->[$i];
|
||||
|
||||
if ($depth == 0 && $line =~ /^\h*member\h+(\S+)\h*\{\h*(?:\R)?$/) {
|
||||
my $mname = $1;
|
||||
my $match = ($domain eq 'all' || $domain eq $mname);
|
||||
|
||||
if ($match) {
|
||||
my $d = 1;
|
||||
for my $j ($i + 1 .. $t_end - 1) {
|
||||
if ($d == 1) {
|
||||
my @m = parse_kv_line($lines->[$j], 'vhAliases');
|
||||
if (@m) {
|
||||
for my $a (split /\h*,\h*/, $m[1]) {
|
||||
next if $a eq '';
|
||||
print "$a\n";
|
||||
}
|
||||
last;
|
||||
}
|
||||
}
|
||||
$d += brace_delta($lines->[$j]);
|
||||
last if $d == 0;
|
||||
}
|
||||
|
||||
return unless $domain eq 'all';
|
||||
}
|
||||
}
|
||||
|
||||
$depth += brace_delta($line);
|
||||
}
|
||||
}
|
||||
|
||||
open my $fh, '<', $file or die "cannot read '$file': $!\n";
|
||||
local $/;
|
||||
my $content = <$fh>;
|
||||
close $fh;
|
||||
|
||||
my @L = split /(?<=\n)/, $content, -1;
|
||||
|
||||
if ($mode eq 'key_add') {
|
||||
my ($block_re, $key, $value) = @args;
|
||||
defined $block_re or die "block_re is required\n";
|
||||
defined $key or die "key is required\n";
|
||||
defined $value or die "value is required\n";
|
||||
key_add(\@L, $block_re, $key, $value);
|
||||
}
|
||||
elsif ($mode eq 'key_set') {
|
||||
my ($block_re, $key, $value) = @args;
|
||||
defined $block_re or die "block_re is required\n";
|
||||
defined $key or die "key is required\n";
|
||||
defined $value or die "value is required\n";
|
||||
key_del(\@L, $block_re, $key, '');
|
||||
key_add(\@L, $block_re, $key, $value);
|
||||
}
|
||||
elsif ($mode eq 'key_mask_set') {
|
||||
my ($block_re, $key, $mask, $value) = @args;
|
||||
defined $block_re or die "block_re is required\n";
|
||||
defined $key or die "key is required\n";
|
||||
defined $mask or die "mask is required\n";
|
||||
defined $value or die "value is required\n";
|
||||
key_del(\@L, $block_re, $key, $mask);
|
||||
key_add(\@L, $block_re, $key, $value);
|
||||
}
|
||||
elsif ($mode eq 'key_del') {
|
||||
my ($block_re, $key, $mask) = @args;
|
||||
defined $block_re or die "block_re is required\n";
|
||||
defined $key or die "key is required\n";
|
||||
$mask //= '';
|
||||
key_del(\@L, $block_re, $key, $mask);
|
||||
}
|
||||
elsif ($mode eq 'key_mask_del') {
|
||||
my ($block_re, $key, $mask) = @args;
|
||||
defined $block_re or die "block_re is required\n";
|
||||
defined $key or die "key is required\n";
|
||||
defined $mask or die "mask is required\n";
|
||||
key_del(\@L, $block_re, $key, $mask);
|
||||
}
|
||||
elsif ($mode eq 'block_add') {
|
||||
my ($header) = @args;
|
||||
defined $header && length $header or die "block header is required\n";
|
||||
block_add(\@L, $header);
|
||||
}
|
||||
elsif ($mode eq 'block_del') {
|
||||
my ($header_re) = @args;
|
||||
defined $header_re && length $header_re or die "block header pattern is required\n";
|
||||
block_del(\@L, $header_re);
|
||||
}
|
||||
elsif ($mode eq 'suspended_list') {
|
||||
print "$_\n" for suspended_list(\@L);
|
||||
exit 0;
|
||||
}
|
||||
elsif ($mode eq 'suspended_del') {
|
||||
my ($domain) = @args;
|
||||
defined $domain && length $domain or die "domain is required\n";
|
||||
suspended_del(\@L, $domain);
|
||||
}
|
||||
elsif ($mode eq 'suspended_add') {
|
||||
my ($domain) = @args;
|
||||
defined $domain && length $domain or die "domain is required\n";
|
||||
suspended_add(\@L, $domain);
|
||||
}
|
||||
elsif ($mode eq 'vhost_list') {
|
||||
my ($type) = @args;
|
||||
vhost_list(\@L, $type // 'all');
|
||||
exit 0;
|
||||
}
|
||||
elsif ($mode eq 'vhost_del') {
|
||||
my ($domain) = @args;
|
||||
defined $domain && length $domain or die "domain is required\n";
|
||||
vhost_del(\@L, $domain);
|
||||
}
|
||||
elsif ($mode eq 'vhost_set') {
|
||||
my ($vhRoot, $domain, @aliases) = @args;
|
||||
defined $vhRoot && length $vhRoot or die "vhRoot is required\n";
|
||||
defined $domain && length $domain or die "domain is required\n";
|
||||
vhost_set(\@L, $vhRoot, $domain, @aliases);
|
||||
}
|
||||
elsif ($mode eq 'vhost_alias') {
|
||||
my ($name) = @args;
|
||||
vhost_alias(\@L, $name);
|
||||
exit 0;
|
||||
}
|
||||
elsif ($mode eq 'member_list') {
|
||||
my ($template, $type) = @args;
|
||||
defined $template && length $template or die "template is required\n";
|
||||
defined $type && length $type or die "type is required\n";
|
||||
member_list(\@L, $template, $type);
|
||||
exit 0;
|
||||
}
|
||||
elsif ($mode eq 'member_del') {
|
||||
my ($template, $domain) = @args;
|
||||
defined $template && length $template or die "template is required\n";
|
||||
defined $domain && length $domain or die "domain is required\n";
|
||||
member_del(\@L, $template, $domain);
|
||||
}
|
||||
elsif ($mode eq 'member_set') {
|
||||
my ($template, $domain, @aliases) = @args;
|
||||
defined $template && length $template or die "template is required\n";
|
||||
defined $domain && length $domain or die "domain is required\n";
|
||||
member_set(\@L, $template, $domain, @aliases);
|
||||
}
|
||||
elsif ($mode eq 'member_alias') {
|
||||
my ($template, $domain) = @args;
|
||||
defined $template && length $template or die "template is required\n";
|
||||
defined $domain && length $domain or die "domain is required\n";
|
||||
member_alias(\@L, $template, $domain);
|
||||
exit 0;
|
||||
}
|
||||
else {
|
||||
die "unknown mode '$mode'\n";
|
||||
}
|
||||
|
||||
$content = join '', @L;
|
||||
$content =~ s/\n{3,}/\n\n/g;
|
||||
|
||||
open my $out, '>', $file or die "cannot write '$file': $!\n";
|
||||
print {$out} $content;
|
||||
close $out or die "cannot close '$file': $!\n";
|
||||
|
||||
exit 0;
|
||||
@@ -0,0 +1,134 @@
|
||||
backupDailyDirPattern="^20[0-9]{2}-[0-9]{2}-[0-9]{2}$backupDailySuffix$"
|
||||
backupArchiveDirPattern="^20[0-9]{2}-[0-9]{2}-[0-9]{2}$backupArchiveSuffix$"
|
||||
|
||||
# Generates a backup destination path; uses $backupFirst for the first backup of the day, $appTime otherwise.
|
||||
# [$1] (path): explicit destination path; returned unchanged if provided.
|
||||
function backupPathGenerate() {
|
||||
local path="$1"
|
||||
|
||||
if [[ -z "$path" ]]; then
|
||||
path="$backupDailyPath/$appDate/$backupFirstDir"
|
||||
[[ -d "$path" ]] && path="$backupDailyPath/$appDate/$appTime"
|
||||
fi
|
||||
|
||||
printf '%s' "$path"
|
||||
}
|
||||
|
||||
# Creates a file backup archive for a site.
|
||||
# $1 (domain): site domain name.
|
||||
# $2 (file): target archive file path.
|
||||
function backupSiteFiles() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local file="$2"
|
||||
[[ -n "$file" ]] || { appError "Target file not specified"; return 1; }
|
||||
|
||||
local compressProgram=""
|
||||
if [[ -n "${pigzThreads:-}" ]] && command -v pigz &>/dev/null; then
|
||||
compressProgram="pigz -p $pigzThreads"
|
||||
fi
|
||||
|
||||
archiveCreate "$olsVhostsPath/$domain" "$file" "$compressProgram"
|
||||
}
|
||||
|
||||
# Creates a database backup for a site.
|
||||
# $1 (domain): site domain name.
|
||||
# $2 (file): target database backup file path.
|
||||
function backupSiteDatabase() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local file="$2"
|
||||
[[ -n "$file" ]] || { appError "Target file not specified"; return 1; }
|
||||
|
||||
local databaseName databaseUser databasePass
|
||||
databaseName=$(siteConfigGet "$domain" "databaseName")
|
||||
[[ -n "$databaseName" ]] || { appError "databaseName not found or empty"; return 1; }
|
||||
databaseUser=$(siteConfigGet "$domain" "databaseUser")
|
||||
[[ -n "$databaseUser" ]] || { appError "databaseUser not found or empty"; return 1; }
|
||||
databasePass=$(siteConfigGet "$domain" "databasePass")
|
||||
[[ -n "$databasePass" ]] || { appError "databasePass not found or empty"; return 1; }
|
||||
|
||||
mariadbMasterExport "$databaseUser" "$databasePass" "$databaseName" "$file"
|
||||
}
|
||||
|
||||
# Creates a full backup for a site: files, database, and OLS vhost config.
|
||||
# $1 (domain): site domain name.
|
||||
# [$2] (path): destination directory; auto-generated if omitted.
|
||||
# [$3] (type): backup type: zip, tar, or archive (defaults to $backupType).
|
||||
function backupSite() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local path
|
||||
path=$(backupPathGenerate "$2")
|
||||
|
||||
local type="${3:-$backupType}"
|
||||
case "$type" in
|
||||
zip)
|
||||
backupSiteFiles "$domain" "$path/$domain.zip" || return 1
|
||||
backupSiteDatabase "$domain" "$path/$domain.sql.zip" || return 1
|
||||
;;
|
||||
tar|archive)
|
||||
backupSiteFiles "$domain" "$path/$domain.tar.gz" || return 1
|
||||
backupSiteDatabase "$domain" "$path/$domain.sql.tar.gz" || return 1
|
||||
;;
|
||||
*)
|
||||
appError "Unknown type: $type"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
cp -f -- "$olsVhostsConfigPath/$domain.conf" "$path/$domain.conf" || {
|
||||
appError "Failed to copy vhost config: $domain"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
# Syncs a local path to a remote server using rsync.
|
||||
# $1 (sourcePath): local path to sync.
|
||||
function storagePathSyncRsync() {
|
||||
local sourcePath="$1"
|
||||
[[ -n "$sourcePath" ]] || { appError "Source path not specified"; return 1; }
|
||||
|
||||
rsync -aH --delete-after --partial --partial-dir=.rsync-partial --password-file="$rsyncPassFile" "$sourcePath" rsync://"$rsyncUri$rsyncPath"
|
||||
}
|
||||
|
||||
# Syncs a local path to a remote server over SSH using rsync.
|
||||
# $1 (sourcePath): local path to sync.
|
||||
function storagePathSyncSSH() {
|
||||
local sourcePath="$1"
|
||||
[[ -n "$sourcePath" ]] || { appError "Source path not specified"; return 1; }
|
||||
|
||||
rsync -aH --delete-after --partial --partial-dir=.rsync-partial -e "ssh -i $sshKeyFile" "$sourcePath" "$sshUser@$sshHost:$sshPath"
|
||||
}
|
||||
|
||||
function storageBackupRemove() {
|
||||
local backup error
|
||||
backup="$1"
|
||||
[[ -n "$backup" ]] || { appError "Backup not specified"; return 1; }
|
||||
|
||||
case "$storageType" in
|
||||
rsync)
|
||||
if ! runError error rsyncDirectoryClean "$rsyncPath/$backup"; then
|
||||
appError "$error"
|
||||
elif ! runError error ftpDirectoryDelete "/$backup"; then
|
||||
appError "$error"
|
||||
fi
|
||||
;;
|
||||
ssh)
|
||||
if ! runError error sshDirectoryDelete "/$backup"; then
|
||||
appError "$error"
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
appError "Unknown storageType: $storageType"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
@@ -0,0 +1,931 @@
|
||||
#!/bin/bash
|
||||
|
||||
diagLabel="[Diag]"
|
||||
diagExcludeVhostsRegex='localhost|Example'
|
||||
|
||||
# Writes a section header to the diag log file and prints it to the info log.
|
||||
# $1 (title): section title text.
|
||||
function diagLogSection() {
|
||||
local title="$1"
|
||||
local padding
|
||||
local logPath
|
||||
|
||||
logInfo "$diagLabel $title"
|
||||
|
||||
padding=$((72 - ${#title}))
|
||||
if (( padding > 0 )); then
|
||||
title="${title} $(printf '.%.s' $(seq 1 "$padding"))"
|
||||
fi
|
||||
|
||||
logPath=$(dirname "$diagFile")
|
||||
[[ -d "$logPath" ]] || mkdir -p "$logPath"
|
||||
|
||||
printf "[ %s ]\n" "$title" >> "$diagFile"
|
||||
}
|
||||
|
||||
# Runs a command and appends its output (stdout and stderr) to the diag file.
|
||||
# $@ (...): command and arguments to execute.
|
||||
function diagCmd() {
|
||||
echo "+ $*" >> "$diagFile"
|
||||
"$@" >> "$diagFile" 2>&1 || true
|
||||
echo >> "$diagFile"
|
||||
}
|
||||
|
||||
# Runs a shell command via bash -lc and appends its output to the diag file; errors ignored.
|
||||
# $@ (...): shell command string to execute.
|
||||
function diagShTry() {
|
||||
echo "+ $*" >> "$diagFile"
|
||||
bash -lc "$*" >> "$diagFile" 2>&1 || true
|
||||
echo >> "$diagFile"
|
||||
}
|
||||
|
||||
# Runs a kubectl command via k3sRun and appends its output to the diag file; errors ignored.
|
||||
# $@ (...): kubectl arguments to pass to k3sRun.
|
||||
function diagK3sTry() {
|
||||
echo "+ k3sRun $*" >> "$diagFile"
|
||||
k3sRun "$@" >> "$diagFile" 2>&1 || true
|
||||
echo >> "$diagFile"
|
||||
}
|
||||
|
||||
# Converts $diagSince (e.g. 4h, 30m, 2d) to a human-readable date argument for the date command.
|
||||
function diagSinceDateArg() {
|
||||
local s="${diagSince:-4h}"
|
||||
|
||||
case "$s" in
|
||||
*m)
|
||||
printf '%s minutes ago' "${s%m}"
|
||||
;;
|
||||
*h)
|
||||
printf '%s hours ago' "${s%h}"
|
||||
;;
|
||||
*d)
|
||||
printf '%s days ago' "${s%d}"
|
||||
;;
|
||||
*)
|
||||
printf '4 hours ago'
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Returns the Unix epoch timestamp corresponding to the $diagSince cutoff time.
|
||||
function diagCutoffEpoch() {
|
||||
date -d "$(diagSinceDateArg)" +%s 2>/dev/null || date -d "4 hours ago" +%s
|
||||
}
|
||||
|
||||
# Returns the HH:MM:SS start time for sar based on $diagSince; falls back to 00:00:00 if in the past.
|
||||
function diagSarStartTime() {
|
||||
local t now
|
||||
|
||||
t="$(date -d "$(diagSinceDateArg)" +%H:%M:%S 2>/dev/null || echo "00:00:00")"
|
||||
now="$(date +%H:%M:%S)"
|
||||
|
||||
if [[ "$t" > "$now" ]]; then
|
||||
printf "00:00:00"
|
||||
else
|
||||
printf "%s" "$t"
|
||||
fi
|
||||
}
|
||||
|
||||
# Writes diagnostic metadata (timestamp, since, hostname, file path) to the diag file.
|
||||
function diagMeta() {
|
||||
diagLogSection "Diagnostic metadata"
|
||||
|
||||
{
|
||||
echo "started_at: $(date -Iseconds)"
|
||||
echo "since: $diagSince"
|
||||
echo "hostname: [SERVER]"
|
||||
echo "file: $diagFile"
|
||||
echo
|
||||
} >> "$diagFile" 2>&1
|
||||
}
|
||||
|
||||
# Replaces hostnames, user paths, and UIDs in the diag file with redacted placeholders.
|
||||
function diagSanitizeReport() {
|
||||
local hostLower
|
||||
local hostFqdn
|
||||
local hostFqdnLower
|
||||
local sanitizeHosts
|
||||
|
||||
[[ -f "$diagFile" ]] || return 0
|
||||
|
||||
hostLower="$(printf '%s' "$hostName" | tr '[:upper:]' '[:lower:]')"
|
||||
hostFqdn="$(hostname -f 2>/dev/null || true)"
|
||||
hostFqdnLower="$(printf '%s' "$hostFqdn" | tr '[:upper:]' '[:lower:]')"
|
||||
|
||||
sanitizeHosts="$(
|
||||
printf '%s\n' "$hostName" "$hostLower" "$hostFqdn" "$hostFqdnLower" | awk 'NF && !seen[$0]++'
|
||||
)"
|
||||
|
||||
SANITIZE_HOSTS="$sanitizeHosts" perl -0pi -e '
|
||||
my $hosts = $ENV{SANITIZE_HOSTS} // "";
|
||||
for my $h (split /\n/, $hosts) {
|
||||
next unless defined $h && length $h;
|
||||
my $q = quotemeta($h);
|
||||
s/$q/[SERVER]/g;
|
||||
}
|
||||
|
||||
s#(/media/)[^/\s]+#$1[USER]#g;
|
||||
s#(/run/user/)[0-9]+#$1[UID]#g;
|
||||
' "$diagFile"
|
||||
}
|
||||
|
||||
# Filters stdin lines, excluding those matching reserved vhost names ($diagExcludeVhostsRegex).
|
||||
function diagGrepExcludeReserved() {
|
||||
local regex="${diagExcludeVhostsRegex:-}"
|
||||
|
||||
if [[ -n "$regex" ]]; then
|
||||
grep -Eiv "$regex"
|
||||
else
|
||||
cat
|
||||
fi
|
||||
}
|
||||
|
||||
# Appends filtered disk usage (df -h, excluding overlay/tmpfs/pod mounts) to the diag file.
|
||||
function diagDfUsage() {
|
||||
{
|
||||
echo "+ df -h filtered"
|
||||
df -h \
|
||||
-x tmpfs \
|
||||
-x devtmpfs \
|
||||
-x overlay \
|
||||
-x squashfs \
|
||||
-x efivarfs \
|
||||
2>/dev/null | awk '
|
||||
NR == 1 { print; next }
|
||||
$6 ~ "^/run/k3s/" { next }
|
||||
$6 ~ "^/var/lib/kubelet/pods/" { next }
|
||||
$6 ~ "^/run/user/" { next }
|
||||
$6 ~ "^/media/" { next }
|
||||
{ print }
|
||||
'
|
||||
echo
|
||||
} >> "$diagFile" 2>&1
|
||||
}
|
||||
|
||||
# Appends filtered inode usage (df -i, excluding overlay/tmpfs/pod mounts) to the diag file.
|
||||
function diagDfInodes() {
|
||||
{
|
||||
echo "+ df -i filtered"
|
||||
df -i \
|
||||
-x tmpfs \
|
||||
-x devtmpfs \
|
||||
-x overlay \
|
||||
-x squashfs \
|
||||
-x efivarfs \
|
||||
2>/dev/null | awk '
|
||||
NR == 1 { print; next }
|
||||
$6 ~ "^/run/k3s/" { next }
|
||||
$6 ~ "^/var/lib/kubelet/pods/" { next }
|
||||
$6 ~ "^/run/user/" { next }
|
||||
$6 ~ "^/media/" { next }
|
||||
{ print }
|
||||
'
|
||||
echo
|
||||
} >> "$diagFile" 2>&1
|
||||
}
|
||||
|
||||
# Appends iostat extended stats (1s interval, 5 samples, loop/sr devices excluded) to the diag file.
|
||||
function diagIostat() {
|
||||
{
|
||||
echo "+ iostat -x -z 1 5 | filter loop/sr devices"
|
||||
|
||||
{
|
||||
iostat -x -z 1 5 2>/dev/null || iostat -x 1 5 2>/dev/null
|
||||
} | awk '
|
||||
/^loop[0-9]+/ { next }
|
||||
/^sr[0-9]+/ { next }
|
||||
{ print }
|
||||
'
|
||||
|
||||
echo
|
||||
} >> "$diagFile" 2>&1
|
||||
}
|
||||
|
||||
# Runs a sar command and appends its output to the diag file; skips gracefully if data is unavailable.
|
||||
# $1 (title): section title for the diag log.
|
||||
# $2 (cmd): sar command string to execute.
|
||||
function diagSarTry() {
|
||||
local title="$1"
|
||||
local cmd="$2"
|
||||
local saFile
|
||||
|
||||
saFile="/var/log/sysstat/sa$(date +%d)"
|
||||
|
||||
diagLogSection "$title"
|
||||
|
||||
{
|
||||
echo "+ $cmd"
|
||||
|
||||
if [[ ! -r "$saFile" ]]; then
|
||||
echo "sar_data_available: no"
|
||||
echo "sar_file: $saFile"
|
||||
echo "hint: enable sysstat data collection if SAR trends are needed"
|
||||
echo
|
||||
return
|
||||
fi
|
||||
|
||||
bash -lc "$cmd" || true
|
||||
echo
|
||||
} >> "$diagFile" 2>&1
|
||||
}
|
||||
|
||||
# Appends a filtered k3s node summary (conditions, capacity, allocated resources) to the diag file.
|
||||
function diagK3sNodeSummary() {
|
||||
{
|
||||
echo "+ k3sRun describe node filtered"
|
||||
|
||||
echo
|
||||
echo "Conditions:"
|
||||
k3sRun describe node 2>/dev/null | awk '
|
||||
/^Conditions:/ { p=1; next }
|
||||
/^Addresses:/ { p=0 }
|
||||
p {
|
||||
if ($1 == "Type" ||
|
||||
$1 == "----" ||
|
||||
$1 == "MemoryPressure" ||
|
||||
$1 == "DiskPressure" ||
|
||||
$1 == "PIDPressure" ||
|
||||
$1 == "Ready") {
|
||||
print
|
||||
}
|
||||
}
|
||||
' || true
|
||||
|
||||
echo
|
||||
echo "Capacity/Allocatable:"
|
||||
k3sRun describe node 2>/dev/null | awk '
|
||||
/^Capacity:/ { p=1; print; next }
|
||||
/^System Info:/ { p=0 }
|
||||
p && $1 !~ /InternalIP|Hostname|Machine|UUID|Boot|ProviderID/ { print }
|
||||
' || true
|
||||
|
||||
echo
|
||||
echo "Allocated resources:"
|
||||
k3sRun describe node 2>/dev/null | awk '
|
||||
/^Allocated resources:/ { p=1; print; next }
|
||||
/^Events:/ { p=0 }
|
||||
p { print }
|
||||
' || true
|
||||
|
||||
echo
|
||||
} >> "$diagFile" 2>&1
|
||||
}
|
||||
|
||||
# Appends a MariaDB slave replication status summary to the diag file.
|
||||
function diagMariaDBReplicaSummary() {
|
||||
local tmp
|
||||
|
||||
tmp="$(mktemp)"
|
||||
|
||||
diagLogSection "MariaDB slave replication summary"
|
||||
|
||||
{
|
||||
echo "+ mariadbSlaveRootQuery SHOW REPLICA STATUS"
|
||||
echo
|
||||
|
||||
mariadbSlaveRootQuery "SHOW REPLICA STATUS\G" > "$tmp" 2>&1 || true
|
||||
|
||||
if grep -q 'Slave_IO_Running:' "$tmp"; then
|
||||
awk -F': ' '
|
||||
/^[[:space:]]*Slave_IO_State:/ { print "Slave_IO_State:", $2 }
|
||||
/^[[:space:]]*Slave_IO_Running:/ { print "Slave_IO_Running:", $2 }
|
||||
/^[[:space:]]*Slave_SQL_Running:/ { print "Slave_SQL_Running:", $2 }
|
||||
/^[[:space:]]*Seconds_Behind_Master:/ { print "Seconds_Behind_Master:", $2 }
|
||||
/^[[:space:]]*Last_IO_Errno:/ { print "Last_IO_Errno:", $2 }
|
||||
/^[[:space:]]*Last_SQL_Errno:/ { print "Last_SQL_Errno:", $2 }
|
||||
/^[[:space:]]*Relay_Log_Space:/ { print "Relay_Log_Space:", $2 }
|
||||
/^[[:space:]]*Read_Master_Log_Pos:/ { print "Read_Master_Log_Pos:", $2 }
|
||||
/^[[:space:]]*Exec_Master_Log_Pos:/ { print "Exec_Master_Log_Pos:", $2 }
|
||||
/^[[:space:]]*Using_Gtid:/ { print "Using_Gtid:", $2 }
|
||||
/^[[:space:]]*Parallel_Mode:/ { print "Parallel_Mode:", $2 }
|
||||
/^[[:space:]]*Slave_SQL_Running_State:/ { print "Slave_SQL_Running_State:", $2 }
|
||||
' "$tmp" || true
|
||||
|
||||
echo
|
||||
rm -f "$tmp"
|
||||
return
|
||||
fi
|
||||
|
||||
: > "$tmp"
|
||||
mariadbSlaveRootQuery "SHOW REPLICA STATUS;" > "$tmp" 2>&1 || true
|
||||
|
||||
if [[ ! -s "$tmp" ]]; then
|
||||
echo "replication_status_available: no"
|
||||
echo
|
||||
rm -f "$tmp"
|
||||
return
|
||||
fi
|
||||
|
||||
awk -F'\t' '
|
||||
NF >= 12 {
|
||||
print "Slave_IO_State:", $1
|
||||
print "Master_Host: [REDACTED]"
|
||||
print "Master_User: [REDACTED]"
|
||||
print "Master_Port:", $4
|
||||
print "Master_Log_File:", $6
|
||||
print "Read_Master_Log_Pos:", $7
|
||||
print "Relay_Log_File:", $8
|
||||
print "Relay_Log_Pos:", $9
|
||||
print "Relay_Master_Log_File:", $10
|
||||
print "Slave_IO_Running:", $11
|
||||
print "Slave_SQL_Running:", $12
|
||||
found=1
|
||||
next
|
||||
}
|
||||
{
|
||||
print
|
||||
}
|
||||
END {
|
||||
if (!found) {
|
||||
print "replication_status_parse: raw fallback"
|
||||
}
|
||||
}
|
||||
' "$tmp" || true
|
||||
|
||||
echo
|
||||
} >> "$diagFile" 2>&1
|
||||
|
||||
rm -f "$tmp"
|
||||
}
|
||||
|
||||
# Parses the MariaDB slow query log since $diagSince and appends a fingerprint summary to the diag file.
|
||||
function diagMariaDBSlowSummary() {
|
||||
local mariadbMasterSlowLog="$mariadbMasterPath/slow.log"
|
||||
local cutoff
|
||||
|
||||
cutoff="$(diagCutoffEpoch)"
|
||||
|
||||
diagLogSection "MariaDB slow query summary only"
|
||||
|
||||
{
|
||||
echo "+ summarize slow log: $mariadbMasterSlowLog since ${diagSince:-4h}"
|
||||
echo
|
||||
|
||||
if [[ ! -r "$mariadbMasterSlowLog" ]]; then
|
||||
echo "slow_log_readable: no"
|
||||
echo
|
||||
return
|
||||
fi
|
||||
|
||||
perl -MTime::Local - "$mariadbMasterSlowLog" "$cutoff" <<'PERL'
|
||||
use strict;
|
||||
use warnings;
|
||||
use Time::Local;
|
||||
|
||||
my ($file, $cutoff) = @ARGV;
|
||||
|
||||
open my $fh, '<', $file or do {
|
||||
print "slow_log_readable: no\n";
|
||||
exit 0;
|
||||
};
|
||||
|
||||
my %fp;
|
||||
my $total = 0;
|
||||
my $max_qt = 0;
|
||||
my $max_rows_examined = 0;
|
||||
my $max_rows_sent = 0;
|
||||
|
||||
my ($active, $ts, $qt, $rows_sent, $rows_examined, $sql);
|
||||
|
||||
sub reset_entry {
|
||||
$active = 0;
|
||||
$ts = 0;
|
||||
$qt = 0;
|
||||
$rows_sent = 0;
|
||||
$rows_examined = 0;
|
||||
$sql = '';
|
||||
}
|
||||
|
||||
sub fingerprint_sql {
|
||||
my ($s) = @_;
|
||||
|
||||
$s =~ s/`[^`]*`/`X`/g;
|
||||
$s =~ s/'(?:\\.|[^'\\])*'/'X'/g;
|
||||
$s =~ s/"(?:\\.|[^"\\])*"/"X"/g;
|
||||
$s =~ s/\b[0-9a-fA-F]{16,}\b/HEX/g;
|
||||
$s =~ s/\b\d+(?:\.\d+)?\b/N/g;
|
||||
$s =~ s/\s+/ /g;
|
||||
$s =~ s/^\s+|\s+$//g;
|
||||
|
||||
return substr($s, 0, 220);
|
||||
}
|
||||
|
||||
sub flush_entry {
|
||||
return unless $active;
|
||||
return if $ts < $cutoff;
|
||||
|
||||
$total++;
|
||||
$max_qt = $qt if $qt > $max_qt;
|
||||
$max_rows_examined = $rows_examined if $rows_examined > $max_rows_examined;
|
||||
$max_rows_sent = $rows_sent if $rows_sent > $max_rows_sent;
|
||||
|
||||
my $f = fingerprint_sql($sql);
|
||||
return unless length $f;
|
||||
|
||||
$fp{$f}{count}++;
|
||||
$fp{$f}{total_time} += $qt;
|
||||
$fp{$f}{total_rows_examined} += $rows_examined;
|
||||
$fp{$f}{max_time} = $qt if !defined($fp{$f}{max_time}) || $qt > $fp{$f}{max_time};
|
||||
}
|
||||
|
||||
reset_entry();
|
||||
|
||||
while (my $line = <$fh>) {
|
||||
chomp $line;
|
||||
|
||||
if ($line =~ /^# Time:\s*(\d{2})(\d{2})(\d{2})\s+(\d{1,2}):(\d{2}):(\d{2})/) {
|
||||
flush_entry();
|
||||
reset_entry();
|
||||
|
||||
my ($yy, $mo, $dd, $hh, $mm, $ss) = ($1, $2, $3, $4, $5, $6);
|
||||
my $yyyy = $yy >= 70 ? 1900 + $yy : 2000 + $yy;
|
||||
|
||||
$ts = timelocal($ss, $mm, $hh, $dd, $mo - 1, $yyyy);
|
||||
$active = 1;
|
||||
next;
|
||||
}
|
||||
|
||||
next unless $active;
|
||||
|
||||
if ($line =~ /^# Query_time:\s*([0-9.]+).*Rows_sent:\s*([0-9]+).*Rows_examined:\s*([0-9]+)/) {
|
||||
$qt = $1 + 0;
|
||||
$rows_sent = $2 + 0;
|
||||
$rows_examined = $3 + 0;
|
||||
next;
|
||||
}
|
||||
|
||||
next if $line =~ /^# User\@Host:/;
|
||||
next if $line =~ /^use `/;
|
||||
next if $line =~ /^SET timestamp=/;
|
||||
next if $line =~ /^#/;
|
||||
next if $line =~ /^\s*$/;
|
||||
|
||||
$sql .= ' ' . $line;
|
||||
}
|
||||
|
||||
flush_entry();
|
||||
|
||||
print "slow_queries_count: $total\n";
|
||||
print "max_query_time: $max_qt\n";
|
||||
print "max_rows_examined: $max_rows_examined\n";
|
||||
print "max_rows_sent: $max_rows_sent\n";
|
||||
print "\n";
|
||||
print "top_fingerprints:\n";
|
||||
|
||||
my $shown = 0;
|
||||
for my $f (
|
||||
sort {
|
||||
$fp{$b}{total_time} <=> $fp{$a}{total_time}
|
||||
||
|
||||
$fp{$b}{total_rows_examined} <=> $fp{$a}{total_rows_examined}
|
||||
} keys %fp
|
||||
) {
|
||||
last if $shown++ >= 10;
|
||||
|
||||
printf(
|
||||
"- count=%d total_time=%.3f max_time=%.3f total_rows_examined=%d sql=%s\n",
|
||||
$fp{$f}{count},
|
||||
$fp{$f}{total_time},
|
||||
$fp{$f}{max_time},
|
||||
$fp{$f}{total_rows_examined},
|
||||
$f
|
||||
);
|
||||
}
|
||||
PERL
|
||||
|
||||
echo
|
||||
} >> "$diagFile" 2>&1
|
||||
}
|
||||
|
||||
# Appends lsphp process count and RSS stats from an OLS pod to the diag file.
|
||||
# $1 (pod): OLS pod name.
|
||||
function diagOlsPodStats() {
|
||||
local pod="$1"
|
||||
|
||||
diagLogSection "OLS $pod lsphp count and RSS"
|
||||
|
||||
{
|
||||
echo "+ k3sRun exec pod/$pod -c openlitespeed -- sh -s <lsphp stats>"
|
||||
|
||||
k3sRun exec -i "pod/$pod" -c openlitespeed -- sh -s <<'SH' || true
|
||||
tmp="$(mktemp)"
|
||||
|
||||
echo "hostname:"
|
||||
hostname 2>/dev/null || true
|
||||
echo
|
||||
|
||||
ps -eo user=,rss=,comm=,args= > "$tmp" 2>/dev/null || ps aux > "$tmp" 2>/dev/null || true
|
||||
|
||||
echo "lsphp count:"
|
||||
awk '
|
||||
/lsphp/ && $0 !~ /awk|grep/ { c++ }
|
||||
END { print c + 0 }
|
||||
' "$tmp"
|
||||
echo
|
||||
|
||||
echo "total lsphp RSS KiB:"
|
||||
awk '
|
||||
/lsphp/ && $0 !~ /awk|grep/ { sum += $2 }
|
||||
END { print sum + 0 }
|
||||
' "$tmp"
|
||||
echo
|
||||
|
||||
echo "total lsphp RSS MiB:"
|
||||
awk '
|
||||
/lsphp/ && $0 !~ /awk|grep/ { sum += $2 }
|
||||
END { printf "%.1f\n", sum / 1024 }
|
||||
' "$tmp"
|
||||
echo
|
||||
|
||||
echo "top RSS lsphp:"
|
||||
awk '
|
||||
/lsphp/ && $0 !~ /awk|grep/ {
|
||||
print
|
||||
}
|
||||
' "$tmp" | sort -k2,2nr | head -10
|
||||
echo
|
||||
|
||||
echo "lsphp by user:"
|
||||
awk '
|
||||
/lsphp/ && $0 !~ /awk|grep/ {
|
||||
count[$1]++
|
||||
rss[$1] += $2
|
||||
}
|
||||
END {
|
||||
for (u in count) {
|
||||
printf "%s count=%d rss_kib=%d rss_mib=%.1f\n", u, count[u], rss[u], rss[u] / 1024
|
||||
}
|
||||
}
|
||||
' "$tmp" | sort -k3,3nr
|
||||
echo
|
||||
|
||||
echo "process count:"
|
||||
awk '
|
||||
NF >= 3 {
|
||||
comm=$3
|
||||
count[comm]++
|
||||
}
|
||||
END {
|
||||
for (c in count) {
|
||||
print count[c], c
|
||||
}
|
||||
}
|
||||
' "$tmp" | sort -nr | head -20
|
||||
echo
|
||||
|
||||
rm -f "$tmp"
|
||||
SH
|
||||
|
||||
echo
|
||||
} >> "$diagFile" 2>&1
|
||||
}
|
||||
|
||||
# Parses the OLS error log since $diagSince and appends categorized error counters to the diag file.
|
||||
function diagOlsErrorSummary() {
|
||||
local openlitespeedErrorLog="$olsLogsPath/error.log"
|
||||
local cutoff
|
||||
|
||||
cutoff="$(diagCutoffEpoch)"
|
||||
|
||||
diagLogSection "OLS error counters summary"
|
||||
|
||||
{
|
||||
echo "+ summarize OLS error log: $openlitespeedErrorLog since ${diagSince:-4h}"
|
||||
echo
|
||||
|
||||
if [[ ! -r "$openlitespeedErrorLog" ]]; then
|
||||
echo "ols_error_log_readable: no"
|
||||
echo
|
||||
return
|
||||
fi
|
||||
|
||||
perl -MTime::Local - "$openlitespeedErrorLog" "$cutoff" <<'PERL'
|
||||
use strict;
|
||||
use warnings;
|
||||
use Time::Local;
|
||||
|
||||
my ($file, $cutoff) = @ARGV;
|
||||
|
||||
open my $fh, '<', $file or do {
|
||||
print "ols_error_log_readable: no\n";
|
||||
exit 0;
|
||||
};
|
||||
|
||||
my %c = (
|
||||
total_lines => 0,
|
||||
warn_count => 0,
|
||||
error_count => 0,
|
||||
hostname_mapping_conflicts => 0,
|
||||
inaccessible_vhost_root => 0,
|
||||
no_request_delivery => 0,
|
||||
connection_reset => 0,
|
||||
memory_errors => 0,
|
||||
too_many_open_files => 0,
|
||||
permission_denied => 0,
|
||||
);
|
||||
|
||||
while (my $line = <$fh>) {
|
||||
my $ts = 0;
|
||||
|
||||
if ($line =~ /^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})/) {
|
||||
$ts = timelocal($6, $5, $4, $3, $2 - 1, $1);
|
||||
}
|
||||
|
||||
next if $ts && $ts < $cutoff;
|
||||
|
||||
$c{total_lines}++;
|
||||
$c{warn_count}++ if $line =~ /\[WARN\]/;
|
||||
$c{error_count}++ if $line =~ /\[ERROR\]/;
|
||||
$c{hostname_mapping_conflicts}++ if $line =~ /Hostname .* is mapped to virtual host .* can.t map to virtual host/;
|
||||
$c{inaccessible_vhost_root}++ if $line =~ /Path for vhost root is not accessible/;
|
||||
$c{no_request_delivery}++ if $line =~ /No request delivery notification has been received/;
|
||||
$c{connection_reset}++ if $line =~ /Connection reset by peer/;
|
||||
$c{memory_errors}++ if $line =~ /OOM|out of memory|Cannot allocate memory/i;
|
||||
$c{too_many_open_files}++ if $line =~ /Too many open files/i;
|
||||
$c{permission_denied}++ if $line =~ /Permission denied/i;
|
||||
}
|
||||
|
||||
for my $k (
|
||||
qw(
|
||||
total_lines
|
||||
warn_count
|
||||
error_count
|
||||
hostname_mapping_conflicts
|
||||
inaccessible_vhost_root
|
||||
no_request_delivery
|
||||
connection_reset
|
||||
memory_errors
|
||||
too_many_open_files
|
||||
permission_denied
|
||||
)
|
||||
) {
|
||||
print "$k: $c{$k}\n";
|
||||
}
|
||||
PERL
|
||||
|
||||
echo
|
||||
} >> "$diagFile" 2>&1
|
||||
}
|
||||
|
||||
# Fetches the OPcache status endpoint four times and appends key metrics to the diag file.
|
||||
function diagOpcacheSummary() {
|
||||
local i tmp
|
||||
|
||||
diagLogSection "OPcache summary"
|
||||
|
||||
{
|
||||
echo "+ curl OPcache endpoint summary"
|
||||
echo "attempts: 4"
|
||||
echo
|
||||
|
||||
for i in 1 2 3 4; do
|
||||
tmp="$(mktemp)"
|
||||
|
||||
curl -kfsS --max-time 10 "$diagOpcacheUrl" > "$tmp" 2>/dev/null || {
|
||||
echo "attempt_$i: failed"
|
||||
rm -f "$tmp"
|
||||
continue
|
||||
}
|
||||
|
||||
echo "attempt_$i:"
|
||||
|
||||
awk '
|
||||
function val(line) {
|
||||
sub(/^.*=>[[:space:]]*/, "", line)
|
||||
if (line == "" || line == "=>") return "false"
|
||||
return line
|
||||
}
|
||||
|
||||
/\[memory_usage\]/ { ctx="memory"; next }
|
||||
/\[interned_strings_usage\]/ { ctx="interned"; next }
|
||||
/\[opcache_statistics\]/ { ctx="stats"; next }
|
||||
/\[jit\]/ { ctx="jit"; next }
|
||||
|
||||
/\[opcache_enabled\]/ { print " opcache_enabled:", val($0) }
|
||||
/\[cache_full\]/ { print " cache_full:", val($0) }
|
||||
/\[restart_pending\]/ { print " restart_pending:", val($0) }
|
||||
/\[restart_in_progress\]/ { print " restart_in_progress:", val($0) }
|
||||
|
||||
ctx=="memory" && /\[used_memory\]/ { print " memory_used:", val($0) }
|
||||
ctx=="memory" && /\[free_memory\]/ { print " memory_free:", val($0) }
|
||||
ctx=="memory" && /\[wasted_memory\]/ { print " memory_wasted:", val($0) }
|
||||
ctx=="memory" && /\[current_wasted_percentage\]/ { print " memory_wasted_pct:", val($0) }
|
||||
|
||||
ctx=="interned" && /\[buffer_size\]/ { print " interned_buffer_size:", val($0) }
|
||||
ctx=="interned" && /\[used_memory\]/ { print " interned_used:", val($0) }
|
||||
ctx=="interned" && /\[free_memory\]/ { print " interned_free:", val($0) }
|
||||
ctx=="interned" && /\[number_of_strings\]/ { print " interned_strings:", val($0) }
|
||||
|
||||
ctx=="stats" && /\[num_cached_scripts\]/ { print " num_cached_scripts:", val($0) }
|
||||
ctx=="stats" && /\[num_cached_keys\]/ { print " num_cached_keys:", val($0) }
|
||||
ctx=="stats" && /\[max_cached_keys\]/ { print " max_cached_keys:", val($0) }
|
||||
ctx=="stats" && /\[hits\]/ { print " hits:", val($0) }
|
||||
ctx=="stats" && /\[misses\]/ { print " misses:", val($0) }
|
||||
ctx=="stats" && /\[oom_restarts\]/ { print " oom_restarts:", val($0) }
|
||||
ctx=="stats" && /\[hash_restarts\]/ { print " hash_restarts:", val($0) }
|
||||
ctx=="stats" && /\[manual_restarts\]/ { print " manual_restarts:", val($0) }
|
||||
ctx=="stats" && /\[opcache_hit_rate\]/ { print " opcache_hit_rate:", val($0) }
|
||||
' "$tmp" || true
|
||||
|
||||
rm -f "$tmp"
|
||||
echo
|
||||
done
|
||||
} >> "$diagFile" 2>&1
|
||||
}
|
||||
|
||||
# Collects system-level diagnostics (uptime, memory, vmstat, iostat, PSI, sar, disk usage).
|
||||
function diagSystem() {
|
||||
diagLogSection "uptime"
|
||||
diagCmd uptime
|
||||
|
||||
diagLogSection "free -m"
|
||||
diagCmd free -m
|
||||
|
||||
diagLogSection "vmstat 1 10"
|
||||
diagCmd vmstat 1 10
|
||||
|
||||
diagLogSection "mpstat 1 10"
|
||||
diagCmd mpstat 1 10
|
||||
|
||||
diagLogSection "iostat -x -z 1 5 filtered"
|
||||
diagIostat
|
||||
|
||||
diagLogSection "cat /proc/pressure/cpu"
|
||||
diagCmd cat /proc/pressure/cpu
|
||||
|
||||
diagLogSection "cat /proc/pressure/memory"
|
||||
diagCmd cat /proc/pressure/memory
|
||||
|
||||
diagLogSection "cat /proc/pressure/io"
|
||||
diagCmd cat /proc/pressure/io
|
||||
|
||||
diagSarTry "sar queue last ${diagSince:-4h}" "sar -q -s '$(diagSarStartTime)'"
|
||||
diagSarTry "sar cpu last ${diagSince:-4h}" "sar -u -s '$(diagSarStartTime)'"
|
||||
diagSarTry "sar memory last ${diagSince:-4h}" "sar -r -s '$(diagSarStartTime)'"
|
||||
diagSarTry "sar swap last ${diagSince:-4h}" "sar -W -s '$(diagSarStartTime)'"
|
||||
|
||||
diagLogSection "Filesystem pressure quick checks"
|
||||
diagDfUsage
|
||||
|
||||
diagLogSection "Filesystem inode quick checks"
|
||||
diagDfInodes
|
||||
}
|
||||
|
||||
# Collects k3s diagnostics (pod top/list, warning events, restart summary, node conditions).
|
||||
function diagK3s() {
|
||||
diagLogSection "Top pod"
|
||||
diagK3sTry top pod
|
||||
|
||||
diagLogSection "Get pod"
|
||||
diagK3sTry get pod
|
||||
|
||||
diagLogSection "Get warning events recent"
|
||||
diagK3sTry get events --field-selector type!=Normal --sort-by=.lastTimestamp
|
||||
|
||||
diagLogSection "Pod restart summary"
|
||||
diagK3sTry get pod -o custom-columns=NAME:.metadata.name,READY:.status.containerStatuses[*].ready,RESTARTS:.status.containerStatuses[*].restartCount,STATE:.status.containerStatuses[*].state.waiting.reason,LAST_STATE:.status.containerStatuses[*].lastState.terminated.reason
|
||||
|
||||
diagLogSection "Node conditions and allocated resources"
|
||||
diagK3sNodeSummary
|
||||
}
|
||||
|
||||
# Collects MariaDB diagnostics (master/slave global status, replication summary, slow queries).
|
||||
function diagMariaDB() {
|
||||
diagLogSection "MariaDB master global status"
|
||||
{
|
||||
echo "+ mariadbMasterRootQuery"
|
||||
mariadbMasterRootQuery "
|
||||
SHOW GLOBAL STATUS WHERE Variable_name IN (
|
||||
'Uptime',
|
||||
'Threads_running',
|
||||
'Threads_connected',
|
||||
'Max_used_connections',
|
||||
'Connections',
|
||||
'Aborted_connects',
|
||||
'Slow_queries',
|
||||
'Questions',
|
||||
'Queries',
|
||||
'Created_tmp_tables',
|
||||
'Created_tmp_disk_tables',
|
||||
'Innodb_buffer_pool_reads',
|
||||
'Innodb_buffer_pool_read_requests',
|
||||
'Innodb_data_reads',
|
||||
'Innodb_data_writes',
|
||||
'Innodb_log_waits',
|
||||
'Open_tables',
|
||||
'Opened_tables',
|
||||
'Table_open_cache_hits',
|
||||
'Table_open_cache_misses',
|
||||
'Table_open_cache_overflows'
|
||||
);
|
||||
" || true
|
||||
echo
|
||||
} >> "$diagFile" 2>&1
|
||||
|
||||
diagLogSection "MariaDB slave global status"
|
||||
{
|
||||
echo "+ mariadbSlaveRootQuery"
|
||||
mariadbSlaveRootQuery "
|
||||
SHOW GLOBAL STATUS WHERE Variable_name IN (
|
||||
'Uptime',
|
||||
'Threads_running',
|
||||
'Threads_connected',
|
||||
'Max_used_connections',
|
||||
'Connections',
|
||||
'Aborted_connects',
|
||||
'Slow_queries',
|
||||
'Questions',
|
||||
'Queries',
|
||||
'Created_tmp_tables',
|
||||
'Created_tmp_disk_tables',
|
||||
'Innodb_buffer_pool_reads',
|
||||
'Innodb_buffer_pool_read_requests',
|
||||
'Innodb_data_reads',
|
||||
'Innodb_data_writes',
|
||||
'Innodb_log_waits',
|
||||
'Slave_running',
|
||||
'Slave_received_heartbeats',
|
||||
'Slave_heartbeat_period',
|
||||
'Slave_open_temp_tables'
|
||||
);
|
||||
" || true
|
||||
echo
|
||||
} >> "$diagFile" 2>&1
|
||||
|
||||
diagMariaDBReplicaSummary
|
||||
diagMariaDBSlowSummary
|
||||
}
|
||||
|
||||
# Collects OpenLiteSpeed diagnostics (pod list, per-pod lsphp stats, error summary, OPcache).
|
||||
function diagOpenLiteSpeed() {
|
||||
local podList pod
|
||||
|
||||
diagLogSection "OpenLiteSpeed pods"
|
||||
diagK3sTry get pod -l app=openlitespeed
|
||||
|
||||
podList="$(k3sPodListApp openlitespeed 2>/dev/null || true)"
|
||||
while read -r pod; do
|
||||
[[ -z "$pod" ]] && continue
|
||||
diagOlsPodStats "$pod"
|
||||
done < <(awk 'NF' <<< "$podList")
|
||||
|
||||
diagOlsErrorSummary
|
||||
diagOpcacheSummary
|
||||
}
|
||||
|
||||
# Generates a full diagnostic report and writes it to $diagFile.
|
||||
# [$1] (diagFile): output file path (defaults to $diagFile).
|
||||
function diagReport() {
|
||||
local diagFile="${1:-$diagFile}"
|
||||
local aiModel="${1:-short}"
|
||||
|
||||
export LC_ALL=C
|
||||
export LANG=C
|
||||
|
||||
local reportPath
|
||||
reportPath=$(dirname "$diagFile")
|
||||
[[ -d "$reportPath" ]] || mkdir -p "$reportPath"
|
||||
|
||||
if [[ "$postfixHost" ]]; then
|
||||
diagExcludeVhostsRegex="$diagExcludeVhostsRegex|${postfixHost//./\\.}"
|
||||
fi
|
||||
|
||||
: > "$diagFile"
|
||||
diagMeta
|
||||
diagSystem
|
||||
diagK3s
|
||||
diagMariaDB
|
||||
diagOpenLiteSpeed
|
||||
diagSanitizeReport
|
||||
}
|
||||
|
||||
# Uploads the diag report file to the remote API.
|
||||
# [$1] (diagFile): report file path (defaults to $diagFile).
|
||||
# [$2] (aiModelId): AI endpoint ID to use (defaults to 1).
|
||||
function diagSend() {
|
||||
local diagFile="${1:-$diagFile}"
|
||||
local aiModelId="${2:-1}"
|
||||
|
||||
logInfo "$diagLabel Sending data..."
|
||||
curl -fsS -X POST "$diagApiUrl" -F "source_type=worker" -F "source_id=$hostUuid" -F "generated_time=$(date +%s)" -F "report_file=@$diagFile" -F "ai_endpoint_id=$aiModelId"
|
||||
logSuccess "$diagLabel Data sent successfully"
|
||||
}
|
||||
|
||||
# Generates the full diagnostic report and sends it to the remote API.
|
||||
# [$1] (aiModel): model name: full or short (defaults to short).
|
||||
function diagRun() {
|
||||
local aiModel="${1:-short}"
|
||||
local aiModelId=2
|
||||
|
||||
case "$aiModel" in
|
||||
full)
|
||||
aiModelId=1
|
||||
;;
|
||||
*)
|
||||
aiModelId=2
|
||||
;;
|
||||
esac
|
||||
|
||||
diagReport "$diagFile"
|
||||
diagSend "$diagFile" "$aiModelId"
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
# Waits for the k3s API to become ready by polling /readyz.
|
||||
# Retries up to $k3sReadyRetries times with $k3sReadySleep seconds between attempts.
|
||||
function k3sReady() {
|
||||
local tries=${k3sReadyRetries:-10}
|
||||
local delay=${k3sReadySleep:-2}
|
||||
local i
|
||||
for ((i=1; i<=tries; i++)); do
|
||||
"$k3sCmd" kubectl get --raw=/readyz >/dev/null 2>&1 && return 0
|
||||
sleep "$delay"
|
||||
done
|
||||
|
||||
appError "k3s API not ready after $tries tries"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Validates a YAML file against the Kubernetes API (dry-run).
|
||||
# $1 (file): path to the YAML file.
|
||||
function k3sYamlCheck() {
|
||||
local file="$1"
|
||||
[[ -n "$file" ]] || { appError "File not specified"; return 1; }
|
||||
[[ -f "$file" ]] || { appError "File not found: $file"; return 1; }
|
||||
|
||||
k3sReady
|
||||
runFail "$k3sCmd" kubectl apply --dry-run=client -f "$file"
|
||||
}
|
||||
|
||||
# Applies a YAML configuration to Kubernetes.
|
||||
# $1 (file): path to the YAML configuration file.
|
||||
function k3sYamlApply() {
|
||||
local file="$1" output error
|
||||
[[ -n "$file" ]] || { appError "File not specified"; return 1; }
|
||||
|
||||
run output error "$k3sCmd" kubectl apply -f "$file" --validate=false --request-timeout=60s || {
|
||||
appError "Error applying YAML: ${error:-$output}"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
# Runs kubectl in $k3sNamespace after ensuring k3s is ready.
|
||||
function k3sRun() {
|
||||
k3sReady || return 1
|
||||
"$k3sCmd" kubectl -n "$k3sNamespace" "$@"
|
||||
}
|
||||
|
||||
# Lists pod names sorted alphabetically, optionally filtered by app label.
|
||||
# [$1] (app): app label value to filter by (optional).
|
||||
function k3sPodList() {
|
||||
local args=()
|
||||
[[ -n "${1-}" ]] && args+=(-l "app=$1")
|
||||
|
||||
k3sRun get pods "${args[@]}" -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' --sort-by='{.metadata.name}'
|
||||
}
|
||||
|
||||
# Lists pod names with their phase (name|phase), optionally filtered by app label.
|
||||
# [$1] (app): app label value to filter by (optional).
|
||||
function k3sPodListStatus() {
|
||||
local args=()
|
||||
[[ -n "${1-}" ]] && args+=(-l "app=$1")
|
||||
|
||||
local raw
|
||||
raw=$(k3sRun get pods "${args[@]}" \
|
||||
-o jsonpath='{range .items[*]}{.metadata.name}{"|"}{.status.phase}{"|"}{range .status.conditions[*]}{.type}{"="}{.status}{" "}{end}{"\n"}{end}' \
|
||||
--sort-by='{.metadata.name}') || return 1
|
||||
|
||||
awk -F'|' 'NF {
|
||||
if ($2 == "Running" && $3 ~ /(^| )DisruptionTarget=True( |$)/)
|
||||
status = "Terminating"
|
||||
else if ($2 == "Running" && $3 !~ /(^| )Ready=True( |$)/)
|
||||
status = "NotReady"
|
||||
else
|
||||
status = $2
|
||||
print $1 "|" status
|
||||
}' <<< "$raw"
|
||||
}
|
||||
|
||||
# Captures the current list of pod names into an array variable.
|
||||
# $1 (varname): name of the array variable to populate.
|
||||
function k3sPodsSnapshot() {
|
||||
local -n __out="$1"
|
||||
mapfile -t __out < <(
|
||||
k3sRun get pods --no-headers 2>/dev/null | awk '{print $1}'
|
||||
)
|
||||
}
|
||||
|
||||
# Waits until all pods not in the baseline snapshot are Running/Succeeded/Completed.
|
||||
# $1 (varname): name of the baseline array variable (from k3sPodsSnapshot).
|
||||
# [$2] (timeout): max wait in seconds (default 240).
|
||||
# [$3] (interval): poll interval in seconds (default 4).
|
||||
function k3sWaitNewPodsReady() {
|
||||
local -n baseline="$1"
|
||||
local timeout="${2:-240}"
|
||||
local interval="${3:-4}"
|
||||
|
||||
local attempts=$(( timeout / interval ))
|
||||
(( attempts < 1 )) && attempts=1
|
||||
|
||||
local i notReady newSeen
|
||||
for ((i=1; i<=attempts; i++)); do
|
||||
mapfile -t _now < <(
|
||||
k3sRun get pods --no-headers 2>/dev/null | awk '{print $1, $3}'
|
||||
)
|
||||
|
||||
notReady=0
|
||||
newSeen=0
|
||||
local ln name status
|
||||
for ln in "${_now[@]}"; do
|
||||
[[ -z "$ln" ]] && continue
|
||||
name="${ln%% *}"
|
||||
status="${ln#* }"
|
||||
|
||||
if ! arrayContains "$name" "${baseline[@]}"; then
|
||||
newSeen=1
|
||||
if [[ "$status" != "Running" && "$status" != "Succeeded" && "$status" != "Completed" ]]; then
|
||||
((notReady++))
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $newSeen -eq 0 || $notReady -eq 0 ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ $i -lt $attempts ]]; then
|
||||
printDotText "pods not ready: $fontBlue$notReady$fontReset" "${fontYellow}waiting$fontReset"
|
||||
sleep "$interval"
|
||||
else
|
||||
printDotText "pods not ready: $fontBlue$notReady$fontReset" "${fontRed}waiting time's up$fontReset"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
# moves a file or directory to quarantine preserving the path relative to $olsVhostsPath
|
||||
function malwareQuarantineMove() {
|
||||
local src="$1"
|
||||
local dst="$malwareQuarantinePath/${appDate}_$appTime/$(dirname -- "${src#"$olsVhostsPath/"}")"
|
||||
|
||||
mkdir -p -- "$dst" || return 1
|
||||
mv -- "$src" "$dst/" || return 1
|
||||
}
|
||||
|
||||
# copies a file to quarantine preserving the path relative to $olsVhostsPath (for partial cleanup)
|
||||
function malwareQuarantineCopy() {
|
||||
local src="$1"
|
||||
local dst="$malwareQuarantinePath/${appDate}_$appTime/$(dirname -- "${src#"$olsVhostsPath/"}")"
|
||||
|
||||
mkdir -p -- "$dst" || return 1
|
||||
cp -p -- "$src" "$dst/" || return 1
|
||||
}
|
||||
|
||||
function malwareUserList() {
|
||||
local output error databaseList
|
||||
if ! run output error mariadbDatabaseListGet; then
|
||||
printDanger "$error"
|
||||
return 1
|
||||
fi
|
||||
mapfile -t databaseList < <(awk 'NF' <<< "$output")
|
||||
(( ${#databaseList[@]} > 0 )) || return
|
||||
|
||||
local inList
|
||||
inList=$(printf "'%s'," "${databaseList[@]}")
|
||||
inList="${inList%,}"
|
||||
|
||||
local sql
|
||||
sql=$(cat << 'EOF'
|
||||
SET SESSION group_concat_max_len = 1000000;
|
||||
SELECT GROUP_CONCAT(CONCAT(
|
||||
"SELECT CONVERT('", t.table_schema, "' USING utf8mb4) COLLATE utf8mb4_unicode_ci AS db_name,",
|
||||
" ID,",
|
||||
" CONVERT(user_login USING utf8mb4) COLLATE utf8mb4_unicode_ci AS user_login,",
|
||||
" user_registered,",
|
||||
" CONVERT('", t.table_name, "' USING utf8mb4) COLLATE utf8mb4_unicode_ci AS table_name",
|
||||
" FROM `", t.table_schema, "`.`", t.table_name, "`",
|
||||
" WHERE user_login LIKE 'adm\\_%' OR user_login LIKE 'admin\\_%' OR user_login LIKE 'administrator\\_%' OR user_login LIKE 'backup\\_%'"
|
||||
) SEPARATOR ' UNION ALL ') INTO @sql
|
||||
FROM information_schema.tables t
|
||||
WHERE t.table_schema IN (__IN_LIST__)
|
||||
AND EXISTS (SELECT 1 FROM information_schema.columns c WHERE c.table_schema=t.table_schema AND c.table_name=t.table_name AND c.column_name='user_login')
|
||||
AND EXISTS (SELECT 1 FROM information_schema.columns c WHERE c.table_schema=t.table_schema AND c.table_name=t.table_name AND c.column_name='ID')
|
||||
AND EXISTS (SELECT 1 FROM information_schema.columns c WHERE c.table_schema=t.table_schema AND c.table_name=t.table_name AND c.column_name='user_registered');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
EOF
|
||||
)
|
||||
sql="${sql/__IN_LIST__/$inList}"
|
||||
|
||||
mariadbMasterRootQuery "$sql"
|
||||
}
|
||||
|
||||
function malwareOptionsList() {
|
||||
local output error databaseList
|
||||
if ! run output error mariadbDatabaseListGet; then
|
||||
printDanger "$error"
|
||||
return 1
|
||||
fi
|
||||
mapfile -t databaseList < <(awk 'NF' <<< "$output")
|
||||
(( ${#databaseList[@]} > 0 )) || return
|
||||
local inList
|
||||
inList=$(printf "'%s'," "${databaseList[@]}")
|
||||
inList="${inList%,}"
|
||||
local sql
|
||||
sql=$(cat << 'EOF'
|
||||
SET SESSION group_concat_max_len = 1000000;
|
||||
SELECT GROUP_CONCAT(CONCAT(
|
||||
"SELECT CONVERT('", t.table_schema, "' USING utf8mb4) COLLATE utf8mb4_unicode_ci AS db_name,",
|
||||
" CONVERT('", t.table_name, "' USING utf8mb4) COLLATE utf8mb4_unicode_ci AS table_name,",
|
||||
" option_id,",
|
||||
" CONVERT(option_name USING utf8mb4) COLLATE utf8mb4_unicode_ci AS option_name",
|
||||
" FROM `", t.table_schema, "`.`", t.table_name, "`",
|
||||
" WHERE option_name LIKE 'sc\\_%'",
|
||||
" OR option_value LIKE 'H4sIAAAAAAA%'"
|
||||
) SEPARATOR ' UNION ALL ') INTO @sql
|
||||
FROM information_schema.tables t
|
||||
WHERE t.table_schema IN (__IN_LIST__)
|
||||
AND EXISTS (SELECT 1 FROM information_schema.columns c WHERE c.table_schema=t.table_schema AND c.table_name=t.table_name AND c.column_name='option_id')
|
||||
AND EXISTS (SELECT 1 FROM information_schema.columns c WHERE c.table_schema=t.table_schema AND c.table_name=t.table_name AND c.column_name='option_name')
|
||||
AND EXISTS (SELECT 1 FROM information_schema.columns c WHERE c.table_schema=t.table_schema AND c.table_name=t.table_name AND c.column_name='option_value');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
EOF
|
||||
)
|
||||
sql="${sql/__IN_LIST__/$inList}"
|
||||
mariadbMasterRootQuery "$sql" | sort -t$'\t' -k1,1 -k2,2 -k4,4
|
||||
}
|
||||
|
||||
function malwareCronList() {
|
||||
local output error databaseList
|
||||
if ! run output error mariadbDatabaseListGet; then
|
||||
printDanger "$error"
|
||||
return 1
|
||||
fi
|
||||
mapfile -t databaseList < <(awk 'NF' <<< "$output")
|
||||
(( ${#databaseList[@]} > 0 )) || return
|
||||
local inList
|
||||
inList=$(printf "'%s'," "${databaseList[@]}")
|
||||
inList="${inList%,}"
|
||||
local sql
|
||||
sql=$(cat << 'EOF'
|
||||
SET SESSION group_concat_max_len = 1000000;
|
||||
SELECT GROUP_CONCAT(CONCAT(
|
||||
"SELECT CONVERT('", t.table_schema, "' USING utf8mb4) COLLATE utf8mb4_unicode_ci AS db_name,",
|
||||
" CONVERT('", t.table_name, "' USING utf8mb4) COLLATE utf8mb4_unicode_ci AS table_name,",
|
||||
" option_id,",
|
||||
" CONVERT(option_name USING utf8mb4) COLLATE utf8mb4_unicode_ci AS option_name",
|
||||
" FROM `", t.table_schema, "`.`", t.table_name, "`",
|
||||
" WHERE option_name = 'cron'",
|
||||
" AND option_value LIKE '%\"sc_cron_fetch\"%'"
|
||||
) SEPARATOR ' UNION ALL ') INTO @sql
|
||||
FROM information_schema.tables t
|
||||
WHERE t.table_schema IN (__IN_LIST__)
|
||||
AND EXISTS (SELECT 1 FROM information_schema.columns c WHERE c.table_schema=t.table_schema AND c.table_name=t.table_name AND c.column_name='option_id')
|
||||
AND EXISTS (SELECT 1 FROM information_schema.columns c WHERE c.table_schema=t.table_schema AND c.table_name=t.table_name AND c.column_name='option_name')
|
||||
AND EXISTS (SELECT 1 FROM information_schema.columns c WHERE c.table_schema=t.table_schema AND c.table_name=t.table_name AND c.column_name='option_value');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
EOF
|
||||
)
|
||||
sql="${sql/__IN_LIST__/$inList}"
|
||||
mariadbMasterRootQuery "$sql" | sort -t$'\t' -k1,1 -k2,2
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
# Executes a command inside the MariaDB master pod.
|
||||
# $@ (...): command and arguments to execute.
|
||||
function mariadbMasterExec() {
|
||||
k3sRun exec pod/"$mariadbMasterKube"-0 -c "$mariadbMasterKube" -- "$@"
|
||||
}
|
||||
|
||||
# Executes a command inside the MariaDB master pod with stdin attached (-i).
|
||||
# $@ (...): command and arguments to execute.
|
||||
function mariadbMasterExecI() {
|
||||
k3sRun exec -i pod/"$mariadbMasterKube"-0 -c "$mariadbMasterKube" -- "$@"
|
||||
}
|
||||
|
||||
# Executes a command inside the MariaDB slave pod.
|
||||
# $@ (...): command and arguments to execute.
|
||||
function mariadbSlaveExec() {
|
||||
k3sRun exec pod/"$mariadbSlaveKube"-0 -c "$mariadbSlaveKube" -- "$@"
|
||||
}
|
||||
|
||||
# Runs a SQL query as root against the specified MariaDB pod.
|
||||
# $1 (target): master|slave
|
||||
# $2 (query): SQL query.
|
||||
# [$3] (showColumn): pass "showColumn" to keep column headers.
|
||||
function mariadbRootQuery() {
|
||||
local target="$1" query="$2"
|
||||
[[ -n "$query" ]] || { appError "Query not specified"; return 1; }
|
||||
|
||||
local execFn
|
||||
case "$target" in
|
||||
master) execFn=mariadbMasterExec ;;
|
||||
slave) execFn=mariadbSlaveExec ;;
|
||||
*) appError "Unknown target: $target"; return 1 ;;
|
||||
esac
|
||||
|
||||
if [[ "${3-}" == "showColumn" ]]; then
|
||||
"$execFn" mariadb -uroot -p"$mariadbRootPass" -e "$query"
|
||||
else
|
||||
"$execFn" mariadb -uroot -p"$mariadbRootPass" -N -e "$query"
|
||||
fi
|
||||
}
|
||||
|
||||
# Runs a SQL query as root against the MariaDB master pod.
|
||||
# $1 (query): SQL query.
|
||||
# [$2] (showColumn): pass "showColumn" to keep column headers.
|
||||
function mariadbMasterRootQuery() { mariadbRootQuery master "$@"; }
|
||||
# Runs a SQL query as root against the MariaDB slave pod.
|
||||
# $1 (query): SQL query.
|
||||
# [$2] (showColumn): pass "showColumn" to keep column headers.
|
||||
function mariadbSlaveRootQuery() { mariadbRootQuery slave "$@"; }
|
||||
|
||||
# Converts a domain name into a MariaDB-safe identifier (max 60 chars).
|
||||
# $1 (domain): domain name.
|
||||
function mariadbDomain2id() {
|
||||
domainToRandom "$1" 60
|
||||
}
|
||||
|
||||
# Reads the MariaDB root password from the Kubernetes secret.
|
||||
function mariadbRootPassSecretGet() {
|
||||
k3sRun get secret "$mariadbKube-secret" -o jsonpath="{.data.root-password}" --ignore-not-found | base64 -d
|
||||
}
|
||||
|
||||
# Saves the MariaDB root password from the Kubernetes secret to a local file.
|
||||
function mariadbRootPassSecretSave() {
|
||||
local password
|
||||
password="$(mariadbRootPassSecretGet)"
|
||||
[[ -n "$password" ]] && printf '%s\n' "$password" > "$appDataPath/$hostName.$mariadbKube"
|
||||
}
|
||||
|
||||
# Lists user-created MariaDB databases (system schemas excluded).
|
||||
function mariadbDatabaseListGet() {
|
||||
local output error
|
||||
run output error mariadbMasterRootQuery "SHOW DATABASES;" || { appError "$error"; return 1; }
|
||||
printf '%s\n' "$output" | awk '!/^(information_schema|performance_schema|mysql|sys)$/'
|
||||
}
|
||||
|
||||
# Lists MariaDB users with Host=% (root and replication_user excluded).
|
||||
function mariadbUserListGet() {
|
||||
local output error
|
||||
run output error mariadbMasterRootQuery "SELECT user FROM mysql.user WHERE Host = '%';" || { appError "$error"; return 1; }
|
||||
printf '%s\n' "$output" | awk '!/^(root|replication_user)$/'
|
||||
}
|
||||
|
||||
# Creates or updates a MariaDB database and user with full privileges.
|
||||
# Revokes privileges from any other users previously assigned to the same database.
|
||||
# $1 (database): database name.
|
||||
# $2 (username): database username.
|
||||
# $3 (password): database user password.
|
||||
function mariadbDatabaseUserSet() {
|
||||
local database="$1"
|
||||
[[ -n "$database" ]] || { appError "Database not specified"; return 1; }
|
||||
local username="$2"
|
||||
[[ -n "$username" ]] || { appError "Username not specified"; return 1; }
|
||||
local password="$3"
|
||||
[[ -n "$password" ]] || { appError "Password not specified"; return 1; }
|
||||
|
||||
mariadbMasterRootQuery "CREATE DATABASE IF NOT EXISTS \`$database\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER IF NOT EXISTS '$username'@'%' IDENTIFIED BY '$password'; ALTER USER '$username'@'%' IDENTIFIED BY '$password'; GRANT ALL PRIVILEGES ON \`$database\`.* TO '$username'@'%' WITH MAX_USER_CONNECTIONS 15;" || {
|
||||
appError "Create/update database user failed: database=$database username=$username"
|
||||
return 1
|
||||
}
|
||||
|
||||
local others
|
||||
others="$(mariadbMasterRootQuery "SELECT DISTINCT User, Host FROM mysql.db WHERE Db='$database' AND NOT (User='$username' AND Host='%');")"
|
||||
while IFS=$'\t' read -r u h; do
|
||||
[[ -z "$u" || -z "$h" ]] && continue
|
||||
mariadbMasterRootQuery "REVOKE ALL PRIVILEGES ON \`$database\`.* FROM '$u'@'$h';" || \
|
||||
appError "Revoke privileges failed: database=$database user=$u host=$h"
|
||||
done <<< "$others"
|
||||
}
|
||||
|
||||
# Checks whether a MariaDB database exists.
|
||||
# $1 (database): database name.
|
||||
function mariadbDatabaseExists() {
|
||||
local database="$1"
|
||||
[[ -n "$database" ]] || { appError "Database not specified"; return 1; }
|
||||
|
||||
local out
|
||||
out="$(mariadbMasterRootQuery "SHOW DATABASES LIKE '$database';" 2>/dev/null | tail -n 1 | tr -d '\r')"
|
||||
[[ "$out" == "$database" ]]
|
||||
}
|
||||
|
||||
# Removes a MariaDB database if it exists.
|
||||
# $1 (database): database name.
|
||||
function mariadbDatabaseRemove() {
|
||||
local database="$1"
|
||||
[[ -n "$database" ]] || { appError "Database not specified"; return 1; }
|
||||
mariadbMasterRootQuery "DROP DATABASE IF EXISTS \`$database\`;"
|
||||
}
|
||||
|
||||
# Removes a MariaDB user from all hosts.
|
||||
# $1 (username): MariaDB username.
|
||||
function mariadbUserRemove() {
|
||||
local username="$1"
|
||||
[[ -n "$username" ]] || { appError "Username not specified"; return 1; }
|
||||
|
||||
local hosts
|
||||
hosts="$(mariadbMasterRootQuery "SELECT Host FROM mysql.user WHERE User='$username';")"
|
||||
while read -r host; do
|
||||
mariadbMasterRootQuery "DROP USER IF EXISTS '$username'@'$host';"
|
||||
done <<< "$hosts"
|
||||
}
|
||||
|
||||
# Removes all tables from a MariaDB database.
|
||||
# $1 (database): database name.
|
||||
# $2 (username): database username.
|
||||
# $3 (password): database user password.
|
||||
function mariadbDatabaseClean() {
|
||||
local database="$1"
|
||||
[[ -n "$database" ]] || { appError "Database not specified"; return 1; }
|
||||
local username="$2"
|
||||
[[ -n "$username" ]] || { appError "Username not specified"; return 1; }
|
||||
local password="$3"
|
||||
[[ -n "$password" ]] || { appError "Password not specified"; return 1; }
|
||||
|
||||
local output error
|
||||
run output error mariadbMasterExec mariadb -u "$username" -p"$password" -N -e "SELECT CONCAT('DROP TABLE \`', table_name, '\`;') FROM information_schema.tables WHERE table_schema = '$database';" || { appError "$error"; return 1; }
|
||||
run output error mariadbMasterExec mariadb -u "$username" -p"$password" -e "USE \`$database\`; SET FOREIGN_KEY_CHECKS = 0; $output SET FOREIGN_KEY_CHECKS = 1;" || { appError "$error"; return 1; }
|
||||
}
|
||||
|
||||
# Base: runs mariadb-dump via the given exec function.
|
||||
# $1 (execFn): mariadbMasterExec|mariadbSlaveExec.
|
||||
# $2 (username), $3 (password), $4 (database|all).
|
||||
# [$5] (file): auto-generated if omitted.
|
||||
# [$6+] (...): optional mariadb-dump parameters.
|
||||
function mariadbExport() {
|
||||
local execFn="$1"
|
||||
[[ -n "$execFn" ]] || { appError "Exec function not specified"; return 1; }
|
||||
shift || true
|
||||
|
||||
local username="$1"
|
||||
[[ -n "$username" ]] || { appError "Username not specified"; return 1; }
|
||||
shift || true
|
||||
|
||||
local password="$1"
|
||||
[[ -n "$password" ]] || { appError "Password not specified"; return 1; }
|
||||
shift || true
|
||||
|
||||
local database="$1"
|
||||
[[ -n "$database" ]] || { appError "Database not specified"; return 1; }
|
||||
shift || true
|
||||
|
||||
local file="$1"
|
||||
if [[ -z "$file" ]]; then
|
||||
[[ "$database" == "all" ]] \
|
||||
&& file="$appDataPath/$mariadbMasterKube/${appDate}_${appTime}_full.sql.tar.gz" \
|
||||
|| file="$appDataPath/$mariadbMasterKube/${appDate}_${appTime}_$database.sql.tar.gz"
|
||||
fi
|
||||
shift || true
|
||||
|
||||
local -a params
|
||||
if [[ $# -gt 0 ]]; then
|
||||
params=("$@")
|
||||
local haveAll=false haveDatabases=false havePositional=false
|
||||
for a in "${params[@]}"; do
|
||||
case "$a" in
|
||||
--all-databases|-A) haveAll=true ;;
|
||||
--databases|-B) haveDatabases=true ;;
|
||||
-*) ;;
|
||||
*) havePositional=true ;;
|
||||
esac
|
||||
done
|
||||
if ! $haveAll && ! $haveDatabases && ! $havePositional; then
|
||||
[[ "$database" == "all" ]] && params+=(--all-databases) || params+=("$database")
|
||||
fi
|
||||
else
|
||||
if [[ "$database" == "all" ]]; then
|
||||
params=(--all-databases --single-transaction --quick --master-data=2 --routines --events)
|
||||
else
|
||||
params=("$database" --single-transaction --quick --routines --events)
|
||||
fi
|
||||
fi
|
||||
|
||||
local filePath fileBase fileSql
|
||||
filePath=$(dirname -- "$file")
|
||||
fileBase=$(basename -- "$file")
|
||||
case "$fileBase" in
|
||||
*.zip) fileSql="${fileBase%.zip}" ;;
|
||||
*.tar.gz) fileSql="${fileBase%.tar.gz}" ;;
|
||||
*) fileSql="$fileBase" ;;
|
||||
esac
|
||||
|
||||
mkdir -p -- "$filePath" || { appError "Failed to create directory: $filePath"; return 1; }
|
||||
rm -f -- "$filePath/$fileSql" "$filePath/$fileBase" &>/dev/null
|
||||
|
||||
local output error
|
||||
runShell output error "$execFn" mariadb-dump -u "$username" -p"$password" "${params[@]}" ">" "$filePath/$fileSql" || {
|
||||
rm -f -- "$filePath/$fileSql" &>/dev/null
|
||||
appError "Error creating dump: ${error:-$output}"
|
||||
return 1
|
||||
}
|
||||
|
||||
case "$fileBase" in
|
||||
*.zip|*.tar.gz|*.tgz)
|
||||
runError error archiveCreate "$filePath/$fileSql" "$file" || {
|
||||
rm -f -- "$filePath/$fileSql" "$filePath/$fileBase" &>/dev/null
|
||||
appError "Error creating archive dump: $error"
|
||||
return 1
|
||||
}
|
||||
rm -f -- "$filePath/$fileSql" &>/dev/null
|
||||
;;
|
||||
esac
|
||||
|
||||
printf '%s' "$file"
|
||||
}
|
||||
|
||||
# Exports from the master pod.
|
||||
# $1 (username), $2 (password), $3 (database|all), [$4] (file), [$5+] (params)
|
||||
function mariadbMasterExport() {
|
||||
mariadbExport mariadbMasterExec "$@"
|
||||
}
|
||||
|
||||
# Exports using root credentials.
|
||||
# $1 (execFn), $2 (database|all), [$3] (file), [$4+] (params)
|
||||
function mariadbRootExport() {
|
||||
mariadbExport "$1" root "$mariadbRootPass" "${@:2}"
|
||||
}
|
||||
|
||||
# Exports from master using root credentials.
|
||||
# [$1] (database|all), [$2] (file), [$3+] (params)
|
||||
function mariadbMasterRootExport() {
|
||||
local target="${1:-all}"
|
||||
local file="${2:-$appDataPath/$mariadbMasterKube/${appDate}_${appTime}_full.sql.tar.gz}"
|
||||
mariadbRootExport mariadbMasterExec "$target" "$file" ${@:3}
|
||||
}
|
||||
|
||||
# Exports from slave using root credentials.
|
||||
# [$1] (database|all), [$2] (file)
|
||||
function mariadbSlaveRootExport() {
|
||||
local target="${1:-all}"
|
||||
local file="${2:-$appDataPath/$mariadbSlaveKube/${appDate}_${appTime}_full.sql.tar.gz}"
|
||||
mariadbRootExport mariadbSlaveExec "$target" "$file" --all-databases --single-transaction --quick --routines --events
|
||||
}
|
||||
|
||||
# Imports a MariaDB dump into a database.
|
||||
# Supports plain SQL, .zip and .tar.gz archives (must contain exactly one file).
|
||||
# $1 (database|all): target database, or "all" to import without selecting a database.
|
||||
# $2 (username): username used for import.
|
||||
# $3 (password): password used for import.
|
||||
# $4 (file): source dump file.
|
||||
function mariadbMasterImport() {
|
||||
local database="$1"
|
||||
[[ -n "$database" ]] || { appError "Database not specified"; return 1; }
|
||||
local username="$2"
|
||||
[[ -n "$username" ]] || { appError "Username not specified"; return 1; }
|
||||
local password="$3"
|
||||
[[ -n "$password" ]] || { appError "Password not specified"; return 1; }
|
||||
local file="$4"
|
||||
[[ -n "$file" ]] || { appError "File not specified"; return 1; }
|
||||
[[ -f "$file" ]] || { appError "File not found: $file"; return 1; }
|
||||
|
||||
local tmpFile
|
||||
if [[ "$file" == *.zip ]]; then
|
||||
local entriesRaw
|
||||
entriesRaw="$(unzip -Z1 "$file" 2>/dev/null)" || { appError "Not a valid zip archive: $file"; return 1; }
|
||||
|
||||
local -a entries=()
|
||||
mapfile -t entries < <(printf '%s\n' "$entriesRaw" | sed '/\/$/d')
|
||||
[[ ${#entries[@]} -eq 1 ]] || { appError "Archive must contain exactly one file: $file"; return 1; }
|
||||
|
||||
tmpFile="$(mktemp)" || { appError "Failed to create temporary file"; return 1; }
|
||||
unzip -p "$file" "${entries[0]}" > "$tmpFile" 2>/dev/null || {
|
||||
rm -f -- "$tmpFile"
|
||||
appError "Failed to extract ZIP archive"
|
||||
return 1
|
||||
}
|
||||
elif [[ "$file" == *.tar.gz ]]; then
|
||||
local entriesRaw
|
||||
entriesRaw="$(tar -tzf "$file" 2>/dev/null)" || { appError "Not a valid tar.gz archive: $file"; return 1; }
|
||||
|
||||
local -a entries=()
|
||||
mapfile -t entries < <(printf '%s\n' "$entriesRaw" | sed '/\/$/d')
|
||||
[[ ${#entries[@]} -eq 1 ]] || { appError "Archive must contain exactly one file: $file"; return 1; }
|
||||
|
||||
tmpFile="$(mktemp)" || { appError "Failed to create temporary file"; return 1; }
|
||||
tar -xOzf "$file" "${entries[0]}" > "$tmpFile" 2>/dev/null || {
|
||||
rm -f -- "$tmpFile"
|
||||
appError "Failed to extract tar.gz archive"
|
||||
return 1
|
||||
}
|
||||
else
|
||||
tmpFile="$file"
|
||||
fi
|
||||
|
||||
if [[ ! -s "$tmpFile" ]]; then
|
||||
[[ "$tmpFile" == "$file" ]] || rm -f -- "$tmpFile"
|
||||
appError "The SQL dump is empty"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local -a mariadbCmd=(mariadbMasterExecI mariadb -u "$username" -p"$password")
|
||||
[[ "$database" != "all" ]] && mariadbCmd+=("$database")
|
||||
|
||||
local output error
|
||||
runShell output error "${mariadbCmd[@]}" "<" "$tmpFile" ">" /dev/null || {
|
||||
[[ "$tmpFile" == "$file" ]] || rm -f -- "$tmpFile"
|
||||
appError "Import failed: ${error:-$output}"
|
||||
return 1
|
||||
}
|
||||
|
||||
[[ "$tmpFile" == "$file" ]] || rm -f -- "$tmpFile"
|
||||
}
|
||||
|
||||
# Imports a MariaDB dump using root credentials.
|
||||
# $1 (database|all): target database, or "all".
|
||||
# $2 (file): source dump file.
|
||||
function mariadbMasterRootImport() {
|
||||
mariadbMasterImport "$1" root "$mariadbRootPass" "${@:2}"
|
||||
}
|
||||
|
||||
# Creates or updates the MariaDB database and user for a site.
|
||||
# $1 (domain): site domain name.
|
||||
function mariadbConfigRebuild() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local databaseName databaseUser databasePass
|
||||
databaseName=$(siteConfigGetOrSet "$domain" "databaseName" "$(mariadbDomain2id "$domain")")
|
||||
databaseUser=$(siteConfigGetOrSet "$domain" "databaseUser" "$(mariadbDomain2id "$domain")")
|
||||
databasePass=$(siteConfigGetOrCreate "$domain" "databasePass")
|
||||
mariadbDatabaseUserSet "$databaseName" "$databaseUser" "$databasePass" || return 1
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
# Restarts the vmagent deployment; errors are intentionally ignored.
|
||||
function metricRestart() {
|
||||
k3sRun rollout restart deployment/"$metricKube"-vmagent || true
|
||||
}
|
||||
|
||||
# Writes Prometheus metrics (build info, vhost counts, domain counts, pod memory) to $metricPromPath/kube.prom.
|
||||
function metricKube() {
|
||||
local fileProm="$metricPromPath/kube.prom"
|
||||
local fileTmp="${fileProm}.tmp"
|
||||
|
||||
mkdir -p -- "$metricPromPath" || return 1
|
||||
|
||||
local vhostAllCount vhostUpCount
|
||||
vhostAllCount=$(openlitespeedConfigVhostList all | grep -c . || true)
|
||||
vhostUpCount=$(openlitespeedConfigVhostList up | grep -c . || true)
|
||||
|
||||
local appTimestamp
|
||||
appTimestamp=$(date -d "$appDate ${appTime//-/:}" +%s)
|
||||
|
||||
local domainAllCount=-1 domainFreeCount=-1
|
||||
[[ -f "$domainsList" ]] && domainAllCount=$(grep -cP '^(?!\s*$)' "$domainsList" || true)
|
||||
[[ -f "$domainsList" ]] && domainFreeCount=$(grep -c '^#' "$domainsList" || true)
|
||||
|
||||
local masterMemReq=-1 masterMemLim=-1
|
||||
local slaveMemReq=-1 slaveMemLim=-1
|
||||
local olsMemReq=-1 olsMemLim=-1
|
||||
local redisMemReq=-1 redisMemLim=-1
|
||||
local _line _req _lim
|
||||
|
||||
_line=$(k3sRun get pod "${mariadbMasterKube}-0" \
|
||||
-o jsonpath="{.spec.containers[?(@.name=='${mariadbMasterKube}')].resources.requests.memory} {.spec.containers[?(@.name=='${mariadbMasterKube}')].resources.limits.memory}" 2>/dev/null)
|
||||
read -r _req _lim <<< "$_line"
|
||||
masterMemReq=$(sizeToBytes "$_req"); masterMemLim=$(sizeToBytes "$_lim")
|
||||
|
||||
_line=$(k3sRun get pod "${mariadbSlaveKube}-0" \
|
||||
-o jsonpath="{.spec.containers[?(@.name=='${mariadbSlaveKube}')].resources.requests.memory} {.spec.containers[?(@.name=='${mariadbSlaveKube}')].resources.limits.memory}" 2>/dev/null)
|
||||
read -r _req _lim <<< "$_line"
|
||||
slaveMemReq=$(sizeToBytes "$_req"); slaveMemLim=$(sizeToBytes "$_lim")
|
||||
|
||||
_line=$(k3sRun get pods -l "app=$olsKube" \
|
||||
-o jsonpath="{.items[0].spec.containers[?(@.name=='${olsKube}')].resources.requests.memory} {.items[0].spec.containers[?(@.name=='${olsKube}')].resources.limits.memory}" 2>/dev/null)
|
||||
read -r _req _lim <<< "$_line"
|
||||
olsMemReq=$(sizeToBytes "$_req"); olsMemLim=$(sizeToBytes "$_lim")
|
||||
|
||||
_line=$(k3sRun get pod "${redisKube}-0" \
|
||||
-o jsonpath="{.spec.containers[?(@.name=='${redisKube}')].resources.requests.memory} {.spec.containers[?(@.name=='${redisKube}')].resources.limits.memory}" 2>/dev/null)
|
||||
read -r _req _lim <<< "$_line"
|
||||
redisMemReq=$(sizeToBytes "$_req"); redisMemLim=$(sizeToBytes "$_lim")
|
||||
|
||||
cat > "$fileTmp" <<EOF
|
||||
# HELP kube_build_info Build and version info
|
||||
# TYPE kube_build_info gauge
|
||||
kube_build_info{version="${appVersion}"} 1
|
||||
|
||||
# HELP kube_start_time Time snapshot
|
||||
# TYPE kube_start_time gauge
|
||||
kube_start_time $appTimestamp
|
||||
|
||||
# HELP kube_vhost_all_count Number of virtual hosts
|
||||
# TYPE kube_vhost_all_count gauge
|
||||
kube_vhost_all_count $vhostAllCount
|
||||
|
||||
# HELP kube_vhost_up_count Number of active virtual hosts
|
||||
# TYPE kube_vhost_up_count gauge
|
||||
kube_vhost_up_count $vhostUpCount
|
||||
|
||||
# HELP kube_domain_all_count Number of domains
|
||||
# TYPE kube_domain_all_count gauge
|
||||
kube_domain_all_count $domainAllCount
|
||||
|
||||
# HELP kube_domain_use_count Number of use domains
|
||||
# TYPE kube_domain_use_count gauge
|
||||
kube_domain_use_count $domainFreeCount
|
||||
|
||||
# HELP kube_pod_memory_request_bytes Pod memory request in bytes
|
||||
# TYPE kube_pod_memory_request_bytes gauge
|
||||
kube_pod_memory_request_bytes{component="mariadb-master"} $masterMemReq
|
||||
kube_pod_memory_request_bytes{component="mariadb-slave"} $slaveMemReq
|
||||
kube_pod_memory_request_bytes{component="openlitespeed"} $olsMemReq
|
||||
kube_pod_memory_request_bytes{component="redis"} $redisMemReq
|
||||
|
||||
# HELP kube_pod_memory_limit_bytes Pod memory limit in bytes
|
||||
# TYPE kube_pod_memory_limit_bytes gauge
|
||||
kube_pod_memory_limit_bytes{component="mariadb-master"} $masterMemLim
|
||||
kube_pod_memory_limit_bytes{component="mariadb-slave"} $slaveMemLim
|
||||
kube_pod_memory_limit_bytes{component="openlitespeed"} $olsMemLim
|
||||
kube_pod_memory_limit_bytes{component="redis"} $redisMemLim
|
||||
EOF
|
||||
mv "$fileTmp" "$fileProm"
|
||||
}
|
||||
|
||||
# Collects lsphp CPU/memory/worker metrics per domain across OLS pods and writes to $metricPromPath/lsphp.prom.
|
||||
function metricLsphp() {
|
||||
local fileProm="$metricPromPath/lsphp.prom"
|
||||
local fileTmp="${fileProm}.tmp"
|
||||
|
||||
mkdir -p -- "$metricPromPath" || return 1
|
||||
|
||||
local podList error
|
||||
run podList error k3sPodListStatus "$olsKube" || return 1
|
||||
|
||||
local pod phase output cpu mem count domain
|
||||
{
|
||||
printf '# HELP lsphp_cpu_percent Total CPU percent used by lsphp workers per domain\n'
|
||||
printf '# TYPE lsphp_cpu_percent gauge\n'
|
||||
printf '# HELP lsphp_memory_percent Total memory percent used by lsphp workers per domain\n'
|
||||
printf '# TYPE lsphp_memory_percent gauge\n'
|
||||
printf '# HELP lsphp_worker_count Number of lsphp worker processes per domain\n'
|
||||
printf '# TYPE lsphp_worker_count gauge\n'
|
||||
|
||||
while IFS='|' read -r pod phase; do
|
||||
[[ "$phase" == "Running" ]] || continue
|
||||
# run output error openlitespeedPodExec "$pod" sh -c "ps aux | grep lsphp | grep -v grep | awk '{u=\$1;cpu[u]+=\$3;mem[u]+=\$4;count[u]++} END {for(u in cpu){name=u;cmd=\"find $olsPodPrivatePath -maxdepth 1 -uid \"u\" -type d 2>/dev/null\";if((cmd|getline dir)>0&&dir!=\"\"){n=split(dir,a,\"/\");name=a[n]};close(cmd);print cpu[u],mem[u],count[u],name}}'" || continue
|
||||
run output error openlitespeedPodExec "$pod" sh -c "ps aux | awk '/lsphp/ && !/nobody/ {u=\$1;cpu[u]+=\$3;mem[u]+=\$4;count[u]++} END {for(u in cpu){name=u;cmd=\"find $olsPodPrivatePath -maxdepth 1 -uid \"u\" -type d 2>/dev/null\";if((cmd|getline dir)>0&&dir!=\"\"){n=split(dir,a,\"/\");name=a[n]};close(cmd);printf \"%.1f\\t%.1f\\t%d\\t%s\\n\",cpu[u],mem[u],count[u],name}}'" || continue
|
||||
|
||||
while IFS=$'\t' read -r cpu mem count domain; do
|
||||
[[ -n "$domain" ]] || continue
|
||||
printf 'lsphp_cpu_percent{domain="%s",pod="%s"} %s\n' "$domain" "$pod" "$cpu"
|
||||
printf 'lsphp_memory_percent{domain="%s",pod="%s"} %s\n' "$domain" "$pod" "$mem"
|
||||
printf 'lsphp_worker_count{domain="%s",pod="%s"} %s\n' "$domain" "$pod" "$count"
|
||||
done <<< "$output"
|
||||
done < <(awk 'NF' <<< "$podList")
|
||||
} > "$fileTmp"
|
||||
|
||||
mv "$fileTmp" "$fileProm"
|
||||
}
|
||||
|
||||
# Collects disk usage per site and sends metrics to the API.
|
||||
function metricSites() {
|
||||
local metricSitesApiUrl="https://api.sodew.ai/api/metrics/sites"
|
||||
local batchId batchTime metricsJson dataJson payload httpCode
|
||||
batchId="$(uuidgen | tr '[:upper:]' '[:lower:]')"
|
||||
batchTime="$(date +%s)"
|
||||
metricsJson='{
|
||||
"site_disk_usage_mb": {"type":"number"}
|
||||
}'
|
||||
dataJson='{}'
|
||||
|
||||
local error vhostList
|
||||
run vhostList error openlitespeedConfigVhostList || {
|
||||
appError "Error retrieving vhost list: $error"
|
||||
return 1
|
||||
}
|
||||
while IFS= read -r domain <&3; do
|
||||
local diskUsage domainJson
|
||||
diskUsage="$(pathSize "$olsVhostsPath/$domain" "mb" || true)"
|
||||
[[ -n "$diskUsage" ]] || continue
|
||||
|
||||
domainJson="$(
|
||||
jq -n --arg diskUsage "$diskUsage" \
|
||||
'{
|
||||
site_disk_usage_mb: { value: (if ($diskUsage | length) > 0 then ($diskUsage | tonumber) else null end) },
|
||||
}'
|
||||
)"
|
||||
dataJson="$(jq --arg domain "$domain" --argjson row "$domainJson" '. + {($domain): $row}' <<< "$dataJson")"
|
||||
done 3< <(awk 'NF' <<< "$vhostList")
|
||||
|
||||
payload="$(
|
||||
jq -n \
|
||||
--arg source_type "worker" \
|
||||
--arg source_id "$hostUuid" \
|
||||
--arg batch_id "$batchId" \
|
||||
--argjson batch_time "$batchTime" \
|
||||
--argjson metrics "$metricsJson" \
|
||||
--argjson data "$dataJson" \
|
||||
'{
|
||||
source_type: $source_type,
|
||||
source_id: $source_id,
|
||||
batch_id: $batch_id,
|
||||
batch_time: $batch_time,
|
||||
metrics: $metrics,
|
||||
data: $data
|
||||
}'
|
||||
)"
|
||||
|
||||
# Sending data...
|
||||
httpCode="$(curl -sS -o /tmp/metrics_sites_response.txt -w '%{http_code}' -X POST "$metricSitesApiUrl" -H 'Content-Type: application/json' --data-binary "$payload")"
|
||||
if [[ "$httpCode" != "204" ]]; then
|
||||
appError "Ingest failed, HTTP $httpCode"
|
||||
cat /tmp/metrics_sites_response.txt >&2 || true
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
@@ -0,0 +1,605 @@
|
||||
# Executes a command inside the OLS deployment container.
|
||||
# $@ (...): command and arguments to execute.
|
||||
function openlitespeedExec() {
|
||||
k3sRun exec deploy/"$olsKube" -c "$olsKube" -- "$@"
|
||||
}
|
||||
|
||||
# Executes a command inside a specific OLS pod.
|
||||
# $1 (pod): pod name.
|
||||
# $2+ (...): command and arguments to execute.
|
||||
function openlitespeedPodExec() {
|
||||
local pod="$1"
|
||||
[[ -n "$pod" ]] || { appError "Pod not specified"; return 1; }
|
||||
shift || true
|
||||
|
||||
k3sRun exec pod/"$pod" -c "$olsKube" -- "$@"
|
||||
}
|
||||
|
||||
# Edits an OpenLiteSpeed configuration file via the OLS config editor script.
|
||||
# $1 (file): config file path.
|
||||
# $2 (mode): edit mode passed to ols-editor.pl.
|
||||
# $3+ (...): additional editor arguments.
|
||||
function openlitespeedConfigEditFile() {
|
||||
local file="$1"
|
||||
local mode="$2"
|
||||
shift 2 || return 1
|
||||
|
||||
[[ -n "$file" ]] || { appError "Config file not specified"; return 1; }
|
||||
[[ -n "$mode" ]] || { appError "Edit mode not specified"; return 1; }
|
||||
|
||||
perl "$appPath/libs/modules/assets/ols-editor.pl" "$file" "$mode" "$@" || {
|
||||
appError "Failed edit config file: $file | mode=$mode"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
# Edits the main OLS config file.
|
||||
# $@ (...): edit mode and arguments.
|
||||
function openlitespeedConfigEdit() {
|
||||
openlitespeedConfigEditFile "$olsConfigFile" "$@"
|
||||
}
|
||||
|
||||
# Adds a value to the main OLS config.
|
||||
function openlitespeedConfigKeyAdd() {
|
||||
openlitespeedConfigEdit key_add "$@"
|
||||
}
|
||||
|
||||
# Sets a value in the main OLS config.
|
||||
function openlitespeedConfigKeySet() {
|
||||
openlitespeedConfigEdit key_set "$@"
|
||||
}
|
||||
|
||||
# Sets a masked value in the main OLS config.
|
||||
function openlitespeedConfigKeyMaskSet() {
|
||||
openlitespeedConfigEdit key_mask_set "$@"
|
||||
}
|
||||
|
||||
# Deletes a value from the main OLS config.
|
||||
function openlitespeedConfigKeyDel() {
|
||||
openlitespeedConfigEdit key_del "$@"
|
||||
}
|
||||
|
||||
# Deletes masked values from the main OLS config.
|
||||
function openlitespeedConfigKeyMaskDel() {
|
||||
openlitespeedConfigEdit key_mask_del "$@"
|
||||
}
|
||||
|
||||
# Adds a generic section to the main OLS config.
|
||||
# $1 (header): section header text, e.g. "listener HTTP".
|
||||
function openlitespeedConfigBlockAdd() {
|
||||
openlitespeedConfigEdit block_add "$@"
|
||||
}
|
||||
|
||||
# Deletes a generic section from the main OLS config.
|
||||
# $1 (header_re): regex pattern matching the section header.
|
||||
function openlitespeedConfigBlockDel() {
|
||||
openlitespeedConfigEdit block_del "$@"
|
||||
}
|
||||
|
||||
# Lists OLS virtual hosts filtered by state.
|
||||
# [$1] (type): filter type: all, up, or down (defaults to all).
|
||||
function openlitespeedConfigVhostList() {
|
||||
local type="${1:-all}"
|
||||
arrayContains "$type" "all" "up" "down" || { appError "Unknown type: $type"; return 1; }
|
||||
|
||||
case "$olsVhostMode" in
|
||||
standalone) openlitespeedConfigEdit vhost_list "$type" || return 1 ;;
|
||||
template) openlitespeedConfigEdit member_list "$olsVhostTemplate" "$type" || return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
function openlitespeedConfigVhostSet() {
|
||||
local domain="$1"
|
||||
[[ -n "$domain" ]] || { appError "Domain not specified"; return 1; }
|
||||
shift
|
||||
|
||||
case "$olsVhostMode" in
|
||||
# standalone) openlitespeedConfigEdit vhost_set "$olsPodVhostsPath/$domain" "$domain" "$@" || return 1 ;;
|
||||
standalone) openlitespeedConfigEdit vhost_set "$olsPodVhostsPath/\$VH_NAME" "$domain" "$@" || return 1 ;;
|
||||
template) openlitespeedConfigEdit member_set "$olsVhostTemplate" "$domain" "$@" || return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Deletes a virtual host entry from the main OLS config.
|
||||
function openlitespeedConfigVhostDel() {
|
||||
local domain="$1"
|
||||
[[ -n "$domain" ]] || { appError "Domain not specified"; return 1; }
|
||||
|
||||
case "$olsVhostMode" in
|
||||
standalone) openlitespeedConfigEdit vhost_del "$domain" || return 1 ;;
|
||||
template) openlitespeedConfigEdit member_del "$olsVhostTemplate" "$domain" || return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Lists aliases assigned to a virtual host.
|
||||
# $1 (domain): site domain name.
|
||||
function openlitespeedConfigVhostAliasList() {
|
||||
local domain="$1"
|
||||
[[ -n "$domain" ]] || { appError "Domain not specified"; return 1; }
|
||||
|
||||
case "$olsVhostMode" in
|
||||
standalone) openlitespeedConfigEdit vhost_alias "$domain" || return 1 ;;
|
||||
template) openlitespeedConfigEdit member_alias "$olsVhostTemplate" "$domain" || return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Lists configured OLS aliases.
|
||||
function openlitespeedConfigAliasList() {
|
||||
case "$olsVhostMode" in
|
||||
standalone) openlitespeedConfigEdit vhost_alias all || return 1 ;;
|
||||
template) openlitespeedConfigEdit member_alias "$olsVhostTemplate" all || return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Marks a virtual host as active.
|
||||
# $1 (domain): site domain name.
|
||||
function openlitespeedConfigVhostUp() {
|
||||
local domain="$1"
|
||||
[[ -n "$domain" ]] || { appError "Domain not specified"; return 1; }
|
||||
openlitespeedConfigEdit suspended_del "$domain" || return 1
|
||||
}
|
||||
|
||||
# Marks a virtual host as suspended.
|
||||
# $1 (domain): site domain name.
|
||||
function openlitespeedConfigVhostDown() {
|
||||
local domain="$1"
|
||||
[[ -n "$domain" ]] || { appError "Domain not specified"; return 1; }
|
||||
openlitespeedConfigEdit suspended_add "$domain" || return 1
|
||||
}
|
||||
|
||||
function openlitespeedConfigRebuild() {
|
||||
local children php block
|
||||
children=$(configGet "$appAssetsPath/openlitespeed/profiles/memory-$kubeMemoryProfile.config" "children")
|
||||
|
||||
fileBackup "$olsConfigFile"
|
||||
|
||||
openlitespeedConfigBlockDel "wsgiDefaults"
|
||||
openlitespeedConfigBlockDel "nodeDefaults"
|
||||
openlitespeedConfigBlockDel "railsDefaults"
|
||||
openlitespeedConfigBlockDel "vh[Tt]emplate\h+centralConfigLog"
|
||||
openlitespeedConfigBlockDel "vh[Tt]emplate\h+EasyRailsWithSuEXEC"
|
||||
openlitespeedConfigBlockDel "vh[Tt]emplate\h+docker"
|
||||
openlitespeedConfigBlockDel "listener\h+Default"
|
||||
openlitespeedConfigBlockDel "virtual[Hh]ost\h+Example"
|
||||
openlitespeedConfigKeySet '' 'useIpInProxyHeader' '2'
|
||||
openlitespeedConfigKeySet 'fileAccessControl' 'checkSymbolLink' '1'
|
||||
openlitespeedConfigKeySet 'accessControl' 'deny' ''
|
||||
openlitespeedConfigKeySet 'accessControl' 'allow' '10.42.0.0/16T, 10.43.0.0/16T'
|
||||
|
||||
local phpList=("" "${olsPhpList[@]}")
|
||||
for php in "${phpList[@]}"; do
|
||||
block="extProcessor lsphp$php"
|
||||
openlitespeedConfigBlockDel "ext[Pp]rocessor lsphp$php"
|
||||
openlitespeedConfigBlockAdd "$block"
|
||||
openlitespeedConfigKeyAdd "$block" 'type' 'lsapi'
|
||||
openlitespeedConfigKeyAdd "$block" 'address' "uds://tmp/lshttpd/lsphp$php.sock"
|
||||
openlitespeedConfigKeyAdd "$block" 'path' "/usr/local/lsws/lsphp$php/bin/lsphp"
|
||||
openlitespeedConfigKeyAdd "$block" 'maxConns' "$children"
|
||||
openlitespeedConfigKeyAdd "$block" 'env' "PHP_LSAPI_CHILDREN=$children"
|
||||
openlitespeedConfigKeyAdd "$block" 'env' 'PHP_INI_SCAN_DIR=:/etc/ols-php-ini:/var/www/private/$VH_NAME/php'
|
||||
openlitespeedConfigKeyAdd "$block" 'env' 'LSAPI_AVOID_FORK=0'
|
||||
openlitespeedConfigKeyAdd "$block" 'env' 'LSAPI_MAX_IDLE=120'
|
||||
openlitespeedConfigKeyAdd "$block" 'env' 'LSAPI_MAX_IDLE_CHILDREN=1'
|
||||
openlitespeedConfigKeyAdd "$block" 'env' 'LSAPI_PGRP_MAX_IDLE=300'
|
||||
openlitespeedConfigKeyAdd "$block" 'env' 'LSAPI_MAX_PROCESS_TIME=300'
|
||||
openlitespeedConfigKeyAdd "$block" 'env' 'LSAPI_SLOW_REQ_MSECS=5000'
|
||||
openlitespeedConfigKeyAdd "$block" 'initTimeout' '60'
|
||||
openlitespeedConfigKeyAdd "$block" 'retryTimeout' '0'
|
||||
openlitespeedConfigKeyAdd "$block" 'persistConn' '1'
|
||||
openlitespeedConfigKeyAdd "$block" 'respBuffer' '0'
|
||||
openlitespeedConfigKeyAdd "$block" 'autoStart' '2'
|
||||
openlitespeedConfigKeyAdd "$block" 'backlog' '100'
|
||||
openlitespeedConfigKeyAdd "$block" 'instances' '1'
|
||||
openlitespeedConfigKeyAdd "$block" 'priority' '0'
|
||||
openlitespeedConfigKeyAdd "$block" 'memSoftLimit' '0'
|
||||
openlitespeedConfigKeyAdd "$block" 'memHardLimit' '0'
|
||||
openlitespeedConfigKeyAdd "$block" 'procSoftLimit' '700'
|
||||
openlitespeedConfigKeyAdd "$block" 'procHardLimit' '800'
|
||||
|
||||
openlitespeedConfigKeyAdd "script[Hh]andler" add "lsapi:lsphp$php lsphp$php"
|
||||
done
|
||||
openlitespeedConfigKeySet "extProcessor\h+lsphp" 'path' 'fcgi-bin/lsphp'
|
||||
|
||||
# module cache
|
||||
block="module cache"
|
||||
openlitespeedConfigBlockDel "module\h+cache"
|
||||
openlitespeedConfigBlockAdd "$block"
|
||||
openlitespeedConfigKeyAdd "$block" 'ls_enabled' '1'
|
||||
openlitespeedConfigKeyAdd "$block" 'checkPrivateCache' '1'
|
||||
openlitespeedConfigKeyAdd "$block" 'checkPublicCache' '1'
|
||||
openlitespeedConfigKeyAdd "$block" 'maxCacheObjSize' '10000000'
|
||||
openlitespeedConfigKeyAdd "$block" 'maxStaleAge' '200'
|
||||
openlitespeedConfigKeyAdd "$block" 'qsCache' '1'
|
||||
openlitespeedConfigKeyAdd "$block" 'reqCookieCache' '1'
|
||||
openlitespeedConfigKeyAdd "$block" 'respCookieCache' '1'
|
||||
openlitespeedConfigKeyAdd "$block" 'ignoreReqCacheCtrl' '1'
|
||||
openlitespeedConfigKeyAdd "$block" 'ignoreRespCacheCtrl' '0'
|
||||
openlitespeedConfigKeyAdd "$block" 'enableCache' '0'
|
||||
openlitespeedConfigKeyAdd "$block" 'expireInSeconds' '3600'
|
||||
openlitespeedConfigKeyAdd "$block" 'enablePrivateCache' '0'
|
||||
openlitespeedConfigKeyAdd "$block" 'privateExpireInSeconds' '3600'
|
||||
}
|
||||
|
||||
function openlitespeedVhostSet() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
shift || true
|
||||
|
||||
local -a aliasesRaw=("$@")
|
||||
local -a aliases=()
|
||||
local aliasRaw alias error
|
||||
for aliasRaw in "${aliasesRaw[@]}"; do
|
||||
alias="$(domainPrepare "$aliasRaw")"
|
||||
[[ -n "$alias" ]] || continue
|
||||
[[ "$alias" == "$domain" ]] && continue
|
||||
runError error domainCheck "$alias" || { appError "$alias: $error"; return 1; }
|
||||
arrayContains "$alias" "${aliases[@]}" || aliases+=("$alias")
|
||||
done
|
||||
|
||||
local vhostAliasList vhostList aliasList
|
||||
run vhostAliasList error openlitespeedConfigVhostAliasList "$domain" || { appError "Failed get list of vhost alias: $error"; return 1; }
|
||||
run vhostList error openlitespeedConfigVhostList || { appError "Failed get list of vhost: $error"; return 1; }
|
||||
run aliasList error openlitespeedConfigAliasList || { appError "Failed get list of alias: $error"; return 1; }
|
||||
|
||||
# For a new domain vhostAliasList is empty, so this covers both create and update
|
||||
local aliasListOther
|
||||
aliasListOther=$(grep -Fvx -f <(printf '%s\n' "$vhostAliasList") <<< "$aliasList" || true)
|
||||
for alias in "${aliases[@]}"; do
|
||||
listContains "$alias" "$vhostList" && { appError "$alias: Is already exists as vhost"; return 1; }
|
||||
listContains "$alias" "$aliasListOther" && { appError "$alias: Is already exists as alias"; return 1; }
|
||||
done
|
||||
|
||||
local aliasValue=''
|
||||
[[ ${#aliases[@]} -gt 0 ]] && aliasValue="$(IFS=','; printf '%s\n' "${aliases[*]}")"
|
||||
|
||||
fileBackup "$olsConfigFile" || return 1
|
||||
openlitespeedConfigVhostSet "$domain" "${aliases[@]}" || return 1
|
||||
|
||||
local domainConfigFile="$appDataPath/config/$domain.config"
|
||||
configSet "$domainConfigFile" alias "$aliasValue" || { appError "Failed set alias in $domainConfigFile"; return 1; }
|
||||
|
||||
if [[ "$olsVhostMode" == "standalone" ]]; then
|
||||
local vHostFile="$olsVhostsConfigPath/$domain.conf"
|
||||
if [[ ! -f "$vHostFile" ]]; then
|
||||
local profileFile memory_limit max_execution_time post_max_size upload_max_filesize
|
||||
profileFile="$appAssetsPath/openlitespeed/profiles/memory-$kubeMemoryProfile.config"
|
||||
[[ -f "$profileFile" ]] || { appError "Profile not found: $profileFile"; return 1; }
|
||||
memory_limit=$(configGet "$profileFile" "memory_limit")
|
||||
max_execution_time=$(configGet "$profileFile" "max_execution_time")
|
||||
post_max_size=$(configGet "$profileFile" "post_max_size")
|
||||
upload_max_filesize=$(configGet "$profileFile" "upload_max_filesize")
|
||||
|
||||
# cp -f -- "$appAssetsPath/openlitespeed/vhost.conf" "$vHostFile" || { appError "Copy vhost template failed"; return 1; }
|
||||
# -e "s|{{domain}}|$domain|g" \
|
||||
cp -f -- "$appAssetsPath/openlitespeed/vhosts/$olsVhostFile" "$vHostFile" || { appError "Copy vhost template failed"; return 1; }
|
||||
sed -i \
|
||||
-e "s|{{memory_limit}}|$memory_limit|g" \
|
||||
-e "s|{{max_execution_time}}|$max_execution_time|g" \
|
||||
-e "s|{{post_max_size}}|$post_max_size|g" \
|
||||
-e "s|{{upload_max_filesize}}|$upload_max_filesize|g" \
|
||||
-- "$vHostFile" || { appError "Template substitution failed: $vHostFile"; return 1; }
|
||||
fi
|
||||
fi
|
||||
|
||||
printf '%s' "$aliasValue"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Removes a virtual host from the OLS main config, listener map, and config files.
|
||||
# Idempotent: safe to call even if the vhost does not exist.
|
||||
# $1 (domain): site domain name.
|
||||
function openlitespeedVhostConfigDel() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
fileBackup "$olsConfigFile" || return 1
|
||||
openlitespeedConfigVhostUp "$domain"
|
||||
openlitespeedConfigVhostDel "$domain"
|
||||
|
||||
if [[ "$olsVhostMode" == "standalone" ]]; then
|
||||
rm -f -- "$olsVhostsConfigPath/$domain.conf" &>/dev/null
|
||||
rm -f -- "$olsVhostsConfigPath/$domain.conf0" &>/dev/null
|
||||
rm -f -- "$olsVhostsConfigPath/$domain.txt" &>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
# Marks a virtual host as active.
|
||||
# $1 (domain): site domain name.
|
||||
function openlitespeedVhostConfigUp() {
|
||||
local domain vhostList error
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
run vhostList error openlitespeedConfigVhostList || return 1
|
||||
listContains "$domain" "$vhostList" || return 1
|
||||
|
||||
openlitespeedConfigVhostUp "$domain"
|
||||
}
|
||||
|
||||
# Marks a virtual host as suspended.
|
||||
# $1 (domain): site domain name.
|
||||
function openlitespeedVhostConfigDown() {
|
||||
local domain vhostList error
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
run vhostList error openlitespeedConfigVhostList || return 1
|
||||
runSilent fileCheckLineLength "$olsConfigFile" 8000 || { appError "fileCheckLineLength 8000"; return 1; }
|
||||
listContains "$domain" "$vhostList" || return 1
|
||||
|
||||
openlitespeedConfigVhostDown "$domain"
|
||||
}
|
||||
|
||||
# Rebuilds filesystem layout, ownership, and permissions for a virtual host.
|
||||
# $1 (vhostPath): virtual host chroot path.
|
||||
# $2 (privatePath): virtual host private path.
|
||||
# $3 (ug): system user/group name for the site.
|
||||
function openlitespeedVhostPermissionSet() {
|
||||
local vhostPath="$1"
|
||||
local privatePath="$2"
|
||||
local ug="$3"
|
||||
[[ -n "$vhostPath" ]] || { appError "vhostPath not specified"; return 1; }
|
||||
[[ -n "$privatePath" ]] || { appError "privatePath not specified"; return 1; }
|
||||
[[ -n "$ug" ]] || { appError "ug not specified"; return 1; }
|
||||
|
||||
# Create site directories
|
||||
mkdir -p -- "$vhostPath"/{www,tmp,session} || { appError "Create vhost directories failed: $vhostPath"; return 1; }
|
||||
|
||||
# Chroot directory: owned by root (required for OpenSSH)
|
||||
chown root:root -- "$vhostPath" || { appError "Change owner of chroot directory failed: $vhostPath"; return 1; }
|
||||
chmod 755 -- "$vhostPath" || { appError "Change permission of chroot directory failed: $vhostPath"; return 1; }
|
||||
|
||||
# Site directories: owned by site user
|
||||
chown -R -- "$ug:$ug" "$vhostPath/www" "$vhostPath/tmp" "$vhostPath/session" || { appError "Change owner of site directories failed: $vhostPath"; return 1; }
|
||||
|
||||
# Permissions: www
|
||||
find "$vhostPath/www" -type d -exec chmod 2750 -- {} + || { appError "Change permissions of www directories failed"; return 1; }
|
||||
find "$vhostPath/www" -type f -exec chmod 640 -- {} + || { appError "Change permissions of www files failed"; return 1; }
|
||||
|
||||
# Permissions: tmp
|
||||
find "$vhostPath/tmp" -type d -exec chmod 700 -- {} + || { appError "Change permissions of tmp directories failed"; return 1; }
|
||||
find "$vhostPath/tmp" -type f -exec chmod 600 -- {} + || { appError "Change permissions of tmp files failed"; return 1; }
|
||||
|
||||
# Permissions: session
|
||||
find "$vhostPath/session" -type d -exec chmod 700 -- {} + || { appError "Change permissions of session directories failed"; return 1; }
|
||||
find "$vhostPath/session" -type f -exec chmod 600 -- {} + || { appError "Change permissions of session files failed"; return 1; }
|
||||
|
||||
# Vhost data directory and bootstrap.php
|
||||
mkdir -p -- "$privatePath" || { appError "Create vhost private directory failed: $privatePath"; return 1; }
|
||||
# mkdir -p -- "$privatePath/php" || { appError "Create vhost private/php directory failed: $privatePath"; return 1; }
|
||||
touch -- "$privatePath/bootstrap.php" || { appError "Create bootstrap.php failed: $privatePath/bootstrap.php"; return 1; }
|
||||
chown -R -- "$ug:$ug" "$privatePath" || { appError "Change owner of vhost private directory failed: $privatePath"; return 1; }
|
||||
find "$privatePath" -type d -exec chmod 700 -- {} + || { appError "Change permissions of vhost private directories failed"; return 1; }
|
||||
find "$privatePath" -type f -exec chmod 600 -- {} + || { appError "Change permissions of vhost private files failed"; return 1; }
|
||||
}
|
||||
|
||||
# Rebuilds ACL rules for a virtual host.
|
||||
# Resets existing ACLs, then grants OLS nobody user read access to www/data and rw to tmp/session.
|
||||
# $1 (vhostPath): virtual host chroot path.
|
||||
# $2 (privatePath): virtual host private path.
|
||||
function openlitespeedVhostAclSet() {
|
||||
local vhostPath="$1"
|
||||
local privatePath="$2"
|
||||
[[ -n "$vhostPath" ]] || { appError "vhostPath not specified"; return 1; }
|
||||
[[ -n "$privatePath" ]] || { appError "privatePath not specified"; return 1; }
|
||||
|
||||
# ACL reset: vhostPath
|
||||
setfacl -R -b -- "$vhostPath" || { appError "Clean ACL rules for vhost path failed: $vhostPath"; return 1; }
|
||||
find "$vhostPath" -type d -exec setfacl -k -- {} + || { appError "Clean default ACL rules for vhost directories failed: $vhostPath"; return 1; }
|
||||
|
||||
# ACL for OLS user nobody: www
|
||||
# Directories: read/traverse + inheritance | Files: read
|
||||
find "$vhostPath/www" -type d -exec setfacl -m u:nobody:rx,m:rx,d:u:nobody:rx,d:m:rx -- {} + || { appError "Set ACL for nobody on www directories failed"; return 1; }
|
||||
find "$vhostPath/www" -type f -exec setfacl -m u:nobody:r,m:r -- {} + || { appError "Set ACL for nobody on www files failed"; return 1; }
|
||||
|
||||
# ACL for OLS user nobody: tmp/session
|
||||
# Directories: rwx + inheritance | Files: rw
|
||||
find "$vhostPath/tmp" "$vhostPath/session" -type d -exec setfacl -m u:nobody:rwx,m:rwx,d:u:nobody:rwx,d:m:rwx -- {} + || { appError "Set ACL for nobody on tmp/session directories failed"; return 1; }
|
||||
find "$vhostPath/tmp" "$vhostPath/session" -type f -exec setfacl -m u:nobody:rw,m:rw -- {} + || { appError "Set ACL for nobody on tmp/session files failed"; return 1; }
|
||||
|
||||
# ACL reset: privatePath
|
||||
setfacl -R -b -- "$privatePath" || { appError "Clean ACL rules for vhost private path failed: $privatePath"; return 1; }
|
||||
find "$privatePath" -type d -exec setfacl -k -- {} + || { appError "Clean default ACL rules for vhost private directories failed: $privatePath"; return 1; }
|
||||
|
||||
# ACL for OLS user nobody: privatePath
|
||||
find "$privatePath" -type d -exec setfacl -m u:nobody:rx,m:rx,d:u:nobody:rx,d:m:rx -- {} + || { appError "Set ACL for nobody on vhost private directories failed"; return 1; }
|
||||
find "$privatePath" -type f -exec setfacl -m u:nobody:r,m:r -- {} + || { appError "Set ACL for nobody on vhost private files failed"; return 1; }
|
||||
}
|
||||
|
||||
# Prints disk and inode quota hard limits for a user on the virtual host filesystem.
|
||||
# Output format: <blockLimit> <inodeLimit>
|
||||
# $1 (user): username or numeric UID.
|
||||
function openlitespeedVhostQuotaGet() {
|
||||
local user="$1"
|
||||
[[ -n "$user" ]] || { appError "User not specified"; return 1; }
|
||||
|
||||
local quotaMount
|
||||
quotaMount=$(findmnt -no TARGET --target "$olsVhostsPath")
|
||||
[[ -n "$quotaMount" ]] || { appError "Quota mount not found: $olsVhostsPath"; return 1; }
|
||||
|
||||
local quotaLimits error
|
||||
run quotaLimits error quota -u "$user" --filesystem "$quotaMount" || { appError "$error"; return 1; }
|
||||
quotaLimits=$(awk 'NR>2 && $1 != "" { print $4, $7; exit }' <<< "$quotaLimits")
|
||||
[[ -n "$quotaLimits" ]] || { appError "Parse quota limits failed: user=$user mount=$quotaMount"; return 1; }
|
||||
|
||||
printf '%s\n' "$quotaLimits"
|
||||
}
|
||||
|
||||
# Sets user disk and inode quota for a virtual host.
|
||||
# Requires user quota to be already enabled and active on the target filesystem.
|
||||
# $1 (domain): site domain name.
|
||||
function openlitespeedVhostQuotaSet() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local ug
|
||||
ug=$(domainToUser "$domain")
|
||||
|
||||
local quotaMount
|
||||
quotaMount=$(findmnt -no TARGET --target "$olsVhostsPath")
|
||||
[[ -n "$quotaMount" ]] || { appError "Quota mount not found: $olsVhostsPath"; return 1; }
|
||||
|
||||
local quotaBlockLimit quotaInodeLimit
|
||||
quotaBlockLimit=$(siteConfigGetOrSet "$domain" "quotaBlockLimit" "$olsQuotaBlockLimit")
|
||||
quotaInodeLimit=$(siteConfigGetOrSet "$domain" "quotaInodeLimit" "$olsQuotaInodeLimit")
|
||||
setquota -u "$ug" "$quotaBlockLimit" "$quotaBlockLimit" "$quotaInodeLimit" "$quotaInodeLimit" "$quotaMount" || {
|
||||
appError "Set quota failed: ug=$ug mount=$quotaMount"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
# Configures XFS project quota for a virtual host.
|
||||
# $1 (vhostPath): virtual host path to assign to an XFS project.
|
||||
# $2 (projectId): numeric XFS project ID.
|
||||
# $3 (projectName): XFS project name.
|
||||
function openlitespeedVhostXfsSet() {
|
||||
local vhostPath="$1"
|
||||
local projectId="$2"
|
||||
local projectName="$3"
|
||||
[[ -n "$vhostPath" ]] || { appError "vhostPath not specified"; return 1; }
|
||||
[[ -n "$projectId" ]] || { appError "projectId not specified"; return 1; }
|
||||
[[ -n "$projectName" ]] || { appError "projectName not specified"; return 1; }
|
||||
|
||||
local quotaFs
|
||||
quotaFs=$(findmnt -no FSTYPE --target "$vhostPath")
|
||||
[[ "$quotaFs" == "xfs" ]] || {
|
||||
appError "XFS quota filesystem mismatch: vhostPath=$vhostPath fs=$quotaFs expected=xfs"
|
||||
return 1
|
||||
}
|
||||
|
||||
local quotaMount
|
||||
quotaMount=$(findmnt -no TARGET --target "$vhostPath")
|
||||
[[ -n "$quotaMount" ]] || { appError "Detect XFS quota mount failed: $vhostPath"; return 1; }
|
||||
[[ "$quotaMount" == '/' || "$vhostPath" == "$quotaMount"/* ]] || {
|
||||
appError "XFS quota mount mismatch: vhostPath=$vhostPath quotaMount=$quotaMount expected=$olsVhostsPath"
|
||||
return 1
|
||||
}
|
||||
|
||||
local quotaOptions
|
||||
quotaOptions=$(findmnt -no OPTIONS --target "$vhostPath")
|
||||
[[ "$quotaOptions" != *noquota* && ( "$quotaOptions" == *prjquota* || "$quotaOptions" == *pquota* ) ]] || {
|
||||
appError "XFS project quota is not enabled: vhostPath=$vhostPath mount=$quotaMount options=$quotaOptions"
|
||||
return 1
|
||||
}
|
||||
|
||||
touch /etc/projects /etc/projid || { appError "Create XFS quota registry files failed"; return 1; }
|
||||
# /etc/projects format: projectId:path
|
||||
sed -i "\#:$vhostPath\$#d" /etc/projects
|
||||
sed -i "\#^$projectId:#d" /etc/projects
|
||||
printf '%s:%s\n' "$projectId" "$vhostPath" >> /etc/projects
|
||||
# /etc/projid format: projectName:projectId
|
||||
sed -i "\#^$projectName:#d" /etc/projid
|
||||
sed -i "\#:$projectId\$#d" /etc/projid
|
||||
printf '%s:%s\n' "$projectName" "$projectId" >> /etc/projid
|
||||
|
||||
xfs_quota -x -c "project -s $projectName" "$quotaMount" || {
|
||||
appError "Set XFS project quota project failed: $projectName $vhostPath"
|
||||
return 1
|
||||
}
|
||||
xfs_quota -x -c "limit -p bhard=$openlitespeedVhostBlockHard ihard=$openlitespeedVhostInodeHard $projectName" "$quotaMount" || {
|
||||
appError "Set XFS project quota limits failed: $projectName"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
# Rebuilds a virtual host: user/group, filesystem layout, permissions, and ACLs.
|
||||
# $1 (domain): site domain name.
|
||||
function openlitespeedVhostRebuild() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local ug vhostPath privatePath
|
||||
ug=$(domainToUser "$domain")
|
||||
vhostPath="$olsVhostsPath/$domain"
|
||||
privatePath="$olsPrivatePath/$domain"
|
||||
|
||||
# User and group
|
||||
if ! getent group "$ug" >/dev/null 2>&1; then
|
||||
groupadd -- "$ug" || { appError "Create group failed: $ug"; return 1; }
|
||||
fi
|
||||
if ! id "$ug" >/dev/null 2>&1; then
|
||||
useradd -M -g "$ug" -d "$vhostPath" -s /usr/sbin/nologin -- "$ug" >/dev/null 2>&1 || { appError "Create user failed: $ug"; return 1; }
|
||||
else
|
||||
usermod -g "$ug" -d "$vhostPath" -s /usr/sbin/nologin -- "$ug" >/dev/null 2>&1 || { appError "Update user failed: $ug"; return 1; }
|
||||
fi
|
||||
|
||||
openlitespeedVhostPermissionSet "$vhostPath" "$privatePath" "$ug" || return 1
|
||||
openlitespeedVhostAclSet "$vhostPath" "$privatePath" || return 1
|
||||
|
||||
# Quota
|
||||
# openlitespeedVhostQuotaSet "$domain" || return 1
|
||||
|
||||
# XFS [ NO USE ! | Only for XFS + prjquota ]
|
||||
# local uId
|
||||
# uId=$(id -u "$ug" 2>/dev/null)
|
||||
# [[ -n "$uId" ]] || { appError "Get user id failed: $ug"; return 1; }
|
||||
# openlitespeedVhostXfsSet "$vhostPath" "$uId" "$domain" || return 1
|
||||
}
|
||||
|
||||
# Edits the OLS WebAdmin config file.
|
||||
# $@ (...): edit mode and arguments.
|
||||
function openlitespeedAdminConfigEdit() {
|
||||
openlitespeedConfigEditFile "$olsAdminPath/admin_config.conf" "$@"
|
||||
}
|
||||
|
||||
# Sets a value in the OLS WebAdmin config.
|
||||
function openlitespeedAdminConfigKeySet() {
|
||||
openlitespeedAdminConfigEdit key_set "$@"
|
||||
}
|
||||
|
||||
# Updates the Traefik middleware IP whitelist for the OLS admin panel from $olsAdminWhiteList.
|
||||
function openlitespeedAdminWhiteList() {
|
||||
local ipList=() whiteList error
|
||||
for i in "${!olsAdminWhiteList[@]}"; do
|
||||
ipList[$i]="\"${olsAdminWhiteList[$i]}\""
|
||||
done
|
||||
whiteList=$(IFS=','; printf '%s' "${ipList[*]}")
|
||||
|
||||
runError error k3sRun patch middleware "$olsKube-admin-allowlist" --type=merge -p "{\"spec\":{\"ipWhiteList\":{\"sourceRange\":[${whiteList}]}}}" \
|
||||
|| { appError "$error"; return 1; }
|
||||
|
||||
printf '%s' "$whiteList"
|
||||
}
|
||||
|
||||
# Restricts OLS admin access to Traefik pod IPs only by updating the admin_config.conf ACL.
|
||||
function openlitespeedAdminAllowList() {
|
||||
local podList
|
||||
podList=$("$k3sCmd" kubectl -n kube-system get pod -l app.kubernetes.io/name=traefik -o jsonpath='{range .items[*]}{.status.podIP}{"\n"}{end}' | awk 'NF')
|
||||
local -a ipList
|
||||
mapfile -t ipList < <(printf '%s\n' "$podList")
|
||||
[[ "${#ipList[@]}" -gt 0 ]] || { appError "Traefik pod IPs not found"; return 1; }
|
||||
|
||||
local allowList
|
||||
allowList=$(IFS=','; printf '%s' "${ipList[*]}")
|
||||
|
||||
fileBackup "$olsAdminPath/admin_config.conf" || return 1
|
||||
openlitespeedAdminConfigKeySet 'accessControl' 'deny' 'ALL' || return 1
|
||||
openlitespeedAdminConfigKeySet 'accessControl' 'allow' "$allowList" || return 1
|
||||
|
||||
printf '%s' "$allowList"
|
||||
}
|
||||
|
||||
# Checks whether user quota support is ready on the virtual host filesystem.
|
||||
function openlitespeedQuotaCheck() {
|
||||
local quotaMount
|
||||
quotaMount=$(findmnt -no TARGET --target "$olsVhostsPath")
|
||||
[[ -n "$quotaMount" ]] || { appError "Quota mount not found: $olsVhostsPath"; return 1; }
|
||||
|
||||
local quotaOptions
|
||||
quotaOptions=$(findmnt -no OPTIONS --target "$quotaMount")
|
||||
[[ "$quotaOptions" == *usrquota* || "$quotaOptions" == *uquota* ]] || {
|
||||
appError "User quota is not enabled: mount=$quotaMount options=$quotaOptions"
|
||||
return 1
|
||||
}
|
||||
|
||||
quotaon -p "$quotaMount" 2>/dev/null | grep -qi "user quota on" || {
|
||||
appError "User quota is not active: mount=$quotaMount"
|
||||
return 1
|
||||
}
|
||||
|
||||
command -v setquota >/dev/null 2>&1 || { appError "setquota command not found"; return 1; }
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
# Executes a command inside the postfix container.
|
||||
function postfixExec() {
|
||||
k3sRun exec -it deploy/"$postfixKube" -c "$postfixKube" -- "$@"
|
||||
}
|
||||
|
||||
# Executes a command inside a specific Postfix pod.
|
||||
# $1 (pod): pod name.
|
||||
# $@ (...): command and arguments to execute.
|
||||
function postfixPodExec() {
|
||||
local pod="$1"
|
||||
[[ -n "$pod" ]] || { appError "Pod not specified"; return 1; }
|
||||
shift || true
|
||||
|
||||
k3sRun exec pod/"$pod" -c "$postfixKube" -- "$@"
|
||||
}
|
||||
|
||||
# Converts a domain name to a random 64-char postfix ID.
|
||||
function postfixDomain2id() {
|
||||
domainToRandom "$1" 64
|
||||
}
|
||||
|
||||
# Reloads the Postfix daemon.
|
||||
function postfixReload() {
|
||||
postfixExec postfix reload
|
||||
}
|
||||
|
||||
# Creates or updates a Postfix SASL user.
|
||||
# $1 (login): SASL username.
|
||||
# $2 (password): password.
|
||||
function postfixUserSet() {
|
||||
local login="$1" password="$2"
|
||||
[[ -n "$login" ]] || { appError "Login not specified"; return 1; }
|
||||
[[ -n "$password" ]] || { appError "Password not specified"; return 1; }
|
||||
|
||||
local error
|
||||
runError error postfixExec \
|
||||
env SASL_LOGIN="$login" SASL_PWD="$password" SASL_DB="/config/sasldb2" SASL_REALM="$postfixDefaultRealm" \
|
||||
sh -lc 'printf "%s\n" "$SASL_PWD" | saslpasswd2 -p -c -f "$SASL_DB" -u "$SASL_REALM" "$SASL_LOGIN"' || {
|
||||
appError "Failed to create/update SASL user $login: $error"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
# Deletes a Postfix SASL user.
|
||||
# $1 (login): SASL username.
|
||||
function postfixUserRemove() {
|
||||
local login="$1"
|
||||
[[ -n "$login" ]] || { appError "Login not specified"; return 1; }
|
||||
|
||||
local error
|
||||
runError error postfixExec saslpasswd2 -d -f "/config/sasldb2" -u "$postfixDefaultRealm" "$login" || {
|
||||
appError "Failed to delete SASL user $login"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
# Lists all SASL users from the sasldb2 database.
|
||||
function postfixUserList() {
|
||||
local output error
|
||||
run output error postfixExec sasldblistusers2 -f /config/sasldb2 || { appError "$error"; return 1; }
|
||||
|
||||
awk -F':' '
|
||||
/^[[:space:]]*$/ { next }
|
||||
{ gsub(/[[:space:]]+$/, "", $1); print $1 }
|
||||
' <<<"$output"
|
||||
}
|
||||
|
||||
# Rebuilds the senders map from the current SASL user list.
|
||||
function postfixSendersRebuild() {
|
||||
local outFile="$postfixConfigPath/$postfixSendersFile"
|
||||
|
||||
local saslList error
|
||||
run saslList error postfixUserList || { appError "Failed to get SASL list: $error"; return 1; }
|
||||
|
||||
local logins
|
||||
logins=$(awk 'NF' <<<"$saslList" | paste -sd ',' -)
|
||||
|
||||
local tmpOut
|
||||
tmpOut=$(mktemp)
|
||||
[[ -n "$logins" ]] && printf '%s %s\n' "$postfixPostmaster" "$logins" > "$tmpOut"
|
||||
mv -f "$tmpOut" "$outFile"
|
||||
}
|
||||
|
||||
function postfixConfigRebuild() {
|
||||
local domain="$1"
|
||||
if [[ -n "$domain" ]]; then
|
||||
postfixDomainSet "$domain"
|
||||
fi
|
||||
|
||||
local error
|
||||
# runError error postfixExec postmap "lmdb:/config/$postfixDomainsFile" || { appError "Failed rebuild $postfixDomainsFile: $error"; return 1; }
|
||||
# runError error postfixExec postmap "lmdb:/config/$postfixAliasesFile" || { appError "Failed rebuild $postfixAliasesFile: $error"; return 1; }
|
||||
runError error postfixExec postmap "lmdb:/config/$postfixSendersFile" || { appError "Failed rebuild $postfixSendersFile: $error"; return 1; }
|
||||
}
|
||||
|
||||
function postfixDomainSet() {
|
||||
local domain="$1"
|
||||
[[ -n "$domain" ]] || { appError "Domain not specified"; return 1; }
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local pfxId
|
||||
pfxId=$(postfixDomain2id "$domain")
|
||||
local postfixUser postfixPass
|
||||
postfixUser=$(siteConfigGetOrSet "$domain" "postfixUser" "$pfxId")
|
||||
postfixPass=$(siteConfigGetOrCreate "$domain" "postfixPass")
|
||||
|
||||
postfixUserSet "$postfixUser" "$postfixPass" || return 1
|
||||
postfixSendersRebuild
|
||||
postfixConfigRebuild
|
||||
}
|
||||
|
||||
function postfixDomainRemove() {
|
||||
local domain="$1"
|
||||
[[ -n "$domain" ]] || { appError "Domain not specified"; return 1; }
|
||||
|
||||
local postfixUser
|
||||
postfixUser=$(siteConfigGet "$domain" "postfixUser")
|
||||
[[ -n "$postfixUser" ]] || { appError "postfixUser not found"; return 1; }
|
||||
|
||||
postfixUserRemove "$postfixUser"
|
||||
postfixSendersRebuild
|
||||
postfixConfigRebuild
|
||||
}
|
||||
|
||||
# Reloads the opendkim daemon.
|
||||
function postfixDkimReload() {
|
||||
postfixExec sh -lc "pkill -HUP -f '/usr/sbin/opendkim' 2>/dev/null" || true
|
||||
}
|
||||
|
||||
# Lists domains that have DKIM key material present.
|
||||
function postfixDkimList() {
|
||||
local f
|
||||
for f in "$postfixDkimPath/keys/"*.txt; do
|
||||
[[ -f "$f" ]] || continue
|
||||
basename -- "$f" .txt
|
||||
done
|
||||
}
|
||||
|
||||
# Generates a DKIM keypair for a domain and updates SigningTable/KeyTable.
|
||||
# $1 (domain): domain name.
|
||||
function postfixDkimAdd() {
|
||||
local domain="$1"
|
||||
[[ -n "$domain" ]] || { appError "Domain not specified"; return 1; }
|
||||
|
||||
if [[ ! -s "$postfixDkimPath/keys/$domain.private" || ! -s "$postfixDkimPath/keys/$domain.txt" ]]; then
|
||||
postfixExec sh -lc "
|
||||
set -e
|
||||
mkdir -p /etc/opendkim/keys
|
||||
opendkim-genkey -b 2048 -r -D '/etc/opendkim/keys' -d '$domain' -s '$postfixDkimSelector'
|
||||
mv -f \"/etc/opendkim/keys/$postfixDkimSelector.private\" \"/etc/opendkim/keys/$domain.private\"
|
||||
mv -f \"/etc/opendkim/keys/$postfixDkimSelector.txt\" \"/etc/opendkim/keys/$domain.txt\"
|
||||
chown opendkim:opendkim \"/etc/opendkim/keys/$domain.private\" \"/etc/opendkim/keys/$domain.txt\" || true
|
||||
chmod 0600 \"/etc/opendkim/keys/$domain.private\"
|
||||
chmod 0644 \"/etc/opendkim/keys/$domain.txt\"
|
||||
" || { appError "Failed to generate DKIM keys for $domain"; return 1; }
|
||||
fi
|
||||
|
||||
local domainEsc="${domain//./\\.}"
|
||||
local selectorEsc="${postfixDkimSelector//./\\.}"
|
||||
sed -i "/^\*@${domainEsc}[[:space:]]/d" "$postfixDkimPath/SigningTable"
|
||||
sed -i "/^${selectorEsc}\._domainkey\.${domainEsc}[[:space:]]/d" "$postfixDkimPath/KeyTable"
|
||||
|
||||
printf '*@%s %s._domainkey.%s\n' "$domain" "$postfixDkimSelector" "$domain" >> "$postfixDkimPath/SigningTable"
|
||||
printf '%s._domainkey.%s %s:%s:/etc/opendkim/keys/%s.private\n' \
|
||||
"$postfixDkimSelector" "$domain" "$domain" "$postfixDkimSelector" "$domain" >> "$postfixDkimPath/KeyTable"
|
||||
|
||||
postfixDkimReload
|
||||
}
|
||||
|
||||
# Ensures DKIM keys exist for a domain and returns the TXT record.
|
||||
# $1 (domain): domain name.
|
||||
function postfixDkimGet() {
|
||||
local domain="$1"
|
||||
[[ -n "$domain" ]] || { appError "Domain not specified"; return 1; }
|
||||
domainCheck "$domain" || return 1
|
||||
postfixDkimAdd "$domain" || return 1
|
||||
cat "$postfixDkimPath/keys/$domain.txt" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Removes DKIM table entries for a domain and reloads opendkim.
|
||||
# $1 (domain): domain name.
|
||||
function postfixDkimRemove() {
|
||||
local domain="$1"
|
||||
[[ -n "$domain" ]] || { appError "Domain not specified"; return 1; }
|
||||
|
||||
local domainEsc="${domain//./\\.}"
|
||||
local selectorEsc="${postfixDkimSelector//./\\.}"
|
||||
sed -i "/^\*@${domainEsc}[[:space:]]/d" "$postfixDkimPath/SigningTable"
|
||||
sed -i "/^${selectorEsc}\._domainkey\.${domainEsc}[[:space:]]/d" "$postfixDkimPath/KeyTable"
|
||||
|
||||
postfixDkimReload
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
# Executes a command inside the Redis master pod.
|
||||
# $@ (...): command and arguments to execute.
|
||||
function redisExec() {
|
||||
k3sRun exec pod/"$redisKube"-0 -c "$redisKube" -- "$@"
|
||||
}
|
||||
|
||||
# Executes a command inside a specific Redis or Redis Sentinel pod.
|
||||
# Container is selected automatically based on the pod name prefix.
|
||||
# $1 (pod): pod name.
|
||||
# $2+ (...): command and arguments to execute.
|
||||
function redisPodExec() {
|
||||
local pod="$1"
|
||||
[[ -n "$pod" ]] || { appError "Pod not specified"; return 1; }
|
||||
shift || true
|
||||
|
||||
local container="$redisKube"
|
||||
[[ "$pod" == "$redisSentinelKube-"* ]] && container="$redisSentinelKube"
|
||||
|
||||
k3sRun exec pod/"$pod" -c "$container" -- "$@"
|
||||
}
|
||||
|
||||
# Runs redis-cli against the master pod, with authentication if configured.
|
||||
# $@ (...): redis-cli arguments.
|
||||
function redisExecCli() {
|
||||
local -a cmd=(redis-cli --no-auth-warning)
|
||||
[[ -n "$redisRootPass" ]] && cmd+=(-a "$redisRootPass")
|
||||
redisExec "${cmd[@]}" "$@"
|
||||
}
|
||||
|
||||
# Runs redis-cli on a specific pod, with authentication and port selected automatically.
|
||||
# Uses port 26379 for Sentinel pods.
|
||||
# $1 (pod): pod name.
|
||||
# $2+ (...): redis-cli arguments.
|
||||
function redisPodExecCli() {
|
||||
local pod="$1"
|
||||
shift || true
|
||||
|
||||
local -a cmd=(redis-cli --no-auth-warning)
|
||||
[[ -n "$redisRootPass" ]] && cmd+=(-a "$redisRootPass")
|
||||
[[ "$pod" == "$redisSentinelKube-"* ]] && cmd+=(-p 26379)
|
||||
|
||||
redisPodExec "$pod" "${cmd[@]}" "$@"
|
||||
}
|
||||
|
||||
# Converts a domain name into a Redis-safe identifier (max 64 chars).
|
||||
# $1 (domain): domain name.
|
||||
function redisDomain2id() {
|
||||
domainToRandom "$1" 64
|
||||
}
|
||||
|
||||
# Reads the Redis root password from the Kubernetes secret.
|
||||
function redisRootPassSecretGet() {
|
||||
k3sRun get secret "$redisKube-secret" -o jsonpath="{.data.root-password}" --ignore-not-found | base64 -d
|
||||
}
|
||||
|
||||
# Saves the Redis root password from the Kubernetes secret to a local file.
|
||||
function redisRootPassSecretSave() {
|
||||
local password
|
||||
password="$(redisRootPassSecretGet)"
|
||||
[[ -n "$password" ]] && printf '%s\n' "$password" > "$appDataPath/$hostName.$redisKube"
|
||||
}
|
||||
|
||||
# Updates the Redis root password across pods. Not yet implemented.
|
||||
function redisRootPassUpdate() {
|
||||
|
||||
printWarning "In progress..."
|
||||
|
||||
# Add update pass in RedisSentinel and HAProxy !!!...
|
||||
|
||||
# k3sRun create secret generic "$redisKube-secret" --from-literal=root-password="$redisRootPass" --dry-run=client -o yaml | k3sRun apply -f -
|
||||
# k3sRun delete pod "$redisKube-0"
|
||||
# sleep 1
|
||||
# k3sRun delete pod "$redisKube-1"
|
||||
# k3sRun rollout restart "sts/$redisSentinelKube"
|
||||
|
||||
# return 1
|
||||
}
|
||||
|
||||
# List Redis users via ACL LIST (names only).
|
||||
function redisUserListGet() {
|
||||
local output error
|
||||
run output error redisExecCli ACL LIST || { appError "$error"; return 1; }
|
||||
printf '%s' "$output" | grep -oP 'user \K\w+'
|
||||
}
|
||||
|
||||
# Flushes all keys from the current Redis database.
|
||||
function redisDatabaseFlush() {
|
||||
runFail redisExecCli FLUSHDB
|
||||
}
|
||||
|
||||
# Persists the ACL user list to disk.
|
||||
function redisUserListSave() {
|
||||
runFail redisExecCli ACL SAVE
|
||||
}
|
||||
|
||||
# Creates or updates a Redis ACL user with password and key pattern.
|
||||
# $1 (username): ACL username.
|
||||
# $2 (password): ACL password.
|
||||
# [$3] (keyPattern): key access pattern (defaults to "username:*").
|
||||
function redisUserSet() {
|
||||
local username="$1"
|
||||
[[ -n "$username" ]] || { appError "Username not specified"; return 1; }
|
||||
local password="$2"
|
||||
[[ -n "$password" ]] || { appError "Password not specified"; return 1; }
|
||||
|
||||
local keyPattern="${3:-${username}:*}"
|
||||
|
||||
local podList error
|
||||
run podList error k3sPodList "$redisKube" || { appError "Get pods list: $error"; return 1; }
|
||||
[[ -n "$podList" ]] || { appError "Pods not found"; return 1; }
|
||||
|
||||
while read -r pod; do
|
||||
runFail redisPodExecCli "$pod" ACL SETUSER "$username" reset on sanitize-payload resetchannels ">$password" "~$keyPattern" -@all +@connection +@string +@keyspace +@sortedset +@scripting +@transaction +info -keys -flushdb -flushall '-script|flush' '-client|kill' '-client|pause' || return 1
|
||||
runFail redisPodExecCli "$pod" ACL SAVE || return 1
|
||||
done <<< "$podList"
|
||||
}
|
||||
|
||||
# Removes a Redis ACL user from all pods.
|
||||
# $1 (username): ACL username.
|
||||
function redisUserRemove() {
|
||||
local username="$1"
|
||||
[[ -n "$username" ]] || { appError "Username not specified"; return 1; }
|
||||
|
||||
local podList error
|
||||
run podList error k3sPodList "$redisKube" || { appError "Get pods list: $error"; return 1; }
|
||||
[[ -n "$podList" ]] || { appError "Pods not found"; return 1; }
|
||||
|
||||
while read -r pod; do
|
||||
runFail redisPodExecCli "$pod" ACL DELUSER "$username" || return 1
|
||||
runFail redisPodExecCli "$pod" ACL SAVE || return 1
|
||||
done <<< "$podList"
|
||||
}
|
||||
|
||||
# Deletes all Redis keys matching the given prefix.
|
||||
# $1 (prefixKey): key prefix to match (e.g. "user:").
|
||||
function redisKeysRemove() {
|
||||
local prefixKey="$1"
|
||||
[[ -n "$prefixKey" ]] || { appError "PrefixKey not specified"; return 1; }
|
||||
|
||||
local output error
|
||||
run output error redisExecCli --scan --pattern "${prefixKey}*" || { appError "$error"; return 1; }
|
||||
[[ -z "$output" ]] && return 0
|
||||
|
||||
local -a keys
|
||||
mapfile -t keys <<< "$output"
|
||||
runFail redisExecCli DEL "${keys[@]}"
|
||||
}
|
||||
|
||||
# Removes all Redis keys belonging to a site's user.
|
||||
# $1 (domain): site domain name.
|
||||
function redisDomainClean() {
|
||||
local domain="$1"
|
||||
domain=$(domainPrepare "$domain")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local redisUser
|
||||
redisUser=$(siteConfigGet "$domain" "redisUser")
|
||||
[[ -n "$redisUser" ]] && redisKeysRemove "$redisUser:"
|
||||
}
|
||||
|
||||
# Creates or updates the Redis ACL user and key pattern for a site.
|
||||
# $1 (domain): site domain name.
|
||||
function redisConfigRebuild() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
fileBackup "$redisPath/$redisFileUsersAcl"
|
||||
|
||||
local redisUser redisPass
|
||||
redisUser=$(siteConfigGetOrSet "$domain" "redisUser" "$(redisDomain2id "$domain")")
|
||||
redisPass=$(siteConfigGetOrCreate "$domain" "redisPass")
|
||||
redisUserSet "$redisUser" "$redisPass" || return 1
|
||||
}
|
||||
|
||||
# Parses raw SENTINEL REPLICAS output into tab-separated lines (name, ip, port, master-host, runid).
|
||||
# Skips disconnected/s_down replicas and deduplicates by runid.
|
||||
# $1 (raw): raw output from `SENTINEL replicas <master>`.
|
||||
function redisSentinelParseReplicas() {
|
||||
local raw="$1"
|
||||
local key value flags
|
||||
local -A slaveData=()
|
||||
local -A seenRunIds=()
|
||||
|
||||
while read -r key && read -r value; do
|
||||
if [[ "$key" == "name" && ${#slaveData[@]} -gt 0 ]]; then
|
||||
flags="${slaveData[flags]}"
|
||||
if [[ -n "${slaveData[runid]}" && "$flags" != *disconnected* && "$flags" != *s_down* ]]; then
|
||||
if [[ -z "${seenRunIds[${slaveData[runid]}]}" ]]; then
|
||||
seenRunIds["${slaveData[runid]}"]=1
|
||||
printf '%s\t%s\t%s\t%s\t%s\n' "${slaveData[name]}" "${slaveData[ip]}" "${slaveData[port]}" "${slaveData[master-host]}" "${slaveData[runid]}"
|
||||
fi
|
||||
fi
|
||||
slaveData=()
|
||||
fi
|
||||
slaveData["$key"]="$value"
|
||||
done <<< "$raw"
|
||||
|
||||
if [[ ${#slaveData[@]} -gt 0 ]]; then
|
||||
flags="${slaveData[flags]}"
|
||||
if [[ -n "${slaveData[runid]}" && "$flags" != *disconnected* && "$flags" != *s_down* ]]; then
|
||||
if [[ -z "${seenRunIds[${slaveData[runid]}]}" ]]; then
|
||||
printf '%s\t%s\t%s\t%s\t%s\n' "${slaveData[name]}" "${slaveData[ip]}" "${slaveData[port]}" "${slaveData[master-host]}" "${slaveData[runid]}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
# Sends a text message to Rocket.Chat via webhook.
|
||||
# Uses global $rocketChatUrl.
|
||||
# $1 (text): message text
|
||||
# [$2] (attachFile): optional path to a file to attach
|
||||
function rocketChatSend() {
|
||||
[[ -n "${rocketChatUrl:-}" ]] || { appError "rocketChatUrl is not configured"; return 1; }
|
||||
|
||||
local text="$1"
|
||||
local attachFile="${2:-}"
|
||||
|
||||
local payload
|
||||
if [[ -n "$attachFile" ]]; then
|
||||
local attachText
|
||||
if [[ -f "$attachFile" ]]; then
|
||||
attachText=$(< "$attachFile")
|
||||
else
|
||||
attachText="_(file not found)_"
|
||||
fi
|
||||
payload=$(jq -nc \
|
||||
--arg text "$text" \
|
||||
--arg title "$attachFile" \
|
||||
--arg attachText "$attachText" \
|
||||
'{"text": $text, "attachments": [{"title": $title, "text": ("```shell\n" + $attachText + "\n```"), "collapsed": true}]}')
|
||||
else
|
||||
payload=$(jq -nc --arg text "$text" '{"text": $text}')
|
||||
fi
|
||||
|
||||
local error
|
||||
runError error curl -sf -X POST -H "Content-Type: application/json" --data "$payload" --retry 2 --retry-delay 1 --max-time 10 -- "$rocketChatUrl" || {
|
||||
appError "Failed to send Rocket.Chat message${error:+: $error}";
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
# Formats a diagnostic report message for Rocket.Chat and outputs it to stdout.
|
||||
# Uses globals: $hostName.
|
||||
# $1 (status): report status (uppercased automatically)
|
||||
# $2 (report): report text (plain multiline string)
|
||||
function rocketChatFormatReport() {
|
||||
local status="${1^^}"
|
||||
local report="$2"
|
||||
|
||||
local -a lines=(
|
||||
"🚨 *KUBE Alert: $status*"
|
||||
""
|
||||
"*Source:* \`$hostName\`"
|
||||
"*Generated at:* \`$(TZ='Europe/Prague' date '+%Y-%m-%d %H:%M:%S') (Europe/Prague)\`"
|
||||
""
|
||||
"$report"
|
||||
)
|
||||
|
||||
printf '%s\n' "${lines[@]}"
|
||||
}
|
||||
|
||||
# Sends a text message to Rocket.Chat via webhook.
|
||||
# Uses global $rocketChatUrl.
|
||||
# $1 (text): message text
|
||||
# [$2] (attachFile): optional path to a file to attach
|
||||
function rocketChatSend2() {
|
||||
[[ -n "${rocketChatUrl:-}" ]] || { appError "rocketChatUrl is not configured"; return 1; }
|
||||
|
||||
local text="$1"
|
||||
local attachFile="${2:-}"
|
||||
|
||||
local payload
|
||||
if [[ -n "$attachFile" ]]; then
|
||||
local attachText
|
||||
if [[ -f "$attachFile" ]]; then
|
||||
attachText=$(< "$attachFile")
|
||||
else
|
||||
attachText="_(file not found)_"
|
||||
fi
|
||||
|
||||
payload=$(jq -nc \
|
||||
--arg text "$text" \
|
||||
--arg title "$attachFile" \
|
||||
--arg attachText "$attachText" \
|
||||
'{"text": ($text + "\n\n<details>\n<summary>📄 " + $title + " (Click to expand)</summary>\n\n```shell\n" + $attachText + "\n```\n</details>")}')
|
||||
else
|
||||
payload=$(jq -nc --arg text "$text" '{"text": $text}')
|
||||
fi
|
||||
|
||||
local error
|
||||
runError error curl -sf -X POST -H "Content-Type: application/json" --data "$payload" --retry 2 --retry-delay 1 --max-time 10 -- "$rocketChatUrl" || {
|
||||
appError "Failed to send Rocket.Chat message${error:+: $error}";
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,460 @@
|
||||
# Reads a value from the site config file.
|
||||
# $1 (domain): site domain name.
|
||||
# $@ (...): arguments passed to configGet.
|
||||
function siteConfigGet() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
shift || true
|
||||
|
||||
configGet "$appDataPath/config/$domain.config" "$@"
|
||||
}
|
||||
|
||||
# Writes a value to the site config file.
|
||||
# $1 (domain): site domain name.
|
||||
# $@ (...): arguments passed to configSet.
|
||||
function siteConfigSet() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
shift || true
|
||||
|
||||
configSet "$appDataPath/config/$domain.config" "$@"
|
||||
}
|
||||
|
||||
# Reads or sets a default value in the site config file.
|
||||
# $1 (domain): site domain name.
|
||||
# $@ (...): arguments passed to configGetOrSet.
|
||||
function siteConfigGetOrSet() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
shift || true
|
||||
|
||||
configGetOrSet "$appDataPath/config/$domain.config" "$@"
|
||||
}
|
||||
|
||||
# Reads or generates a new value in the site config file.
|
||||
# $1 (domain): site domain name.
|
||||
# $@ (...): arguments passed to configGetOrCreate.
|
||||
function siteConfigGetOrCreate() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
shift || true
|
||||
|
||||
configGetOrCreate "$appDataPath/config/$domain.config" "$@"
|
||||
}
|
||||
|
||||
# Creates a new site: validates uniqueness, provisions OLS/MariaDB/Redis/Postfix, and imports files and DB.
|
||||
# $1 (domain): site domain name.
|
||||
# $2 (sourceFiles): path to the site files directory or archive.
|
||||
# [$3] (sourceDatabase): path to a SQL dump file to import (optional).
|
||||
function siteAdd() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local sourceFiles="$2"
|
||||
[[ -n "$sourceFiles" ]] || { appError "Source files not specified"; return 1; }
|
||||
[[ -e "$sourceFiles" ]] || { appError "Source files not found: $sourceFiles"; return 1; }
|
||||
|
||||
local sourceDatabase="$3"
|
||||
if [[ -n "$sourceDatabase" && ! -f "$sourceDatabase" ]]; then
|
||||
appError "Source database not found: $sourceDatabase"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local targetPath="$olsVhostsPath/$domain"
|
||||
[[ ! -e "$targetPath" ]] || { appError "The directory or file exists: $targetPath"; return 1; }
|
||||
|
||||
# OpenLiteSpeed
|
||||
local vhostList aliasList error
|
||||
if ! run vhostList error openlitespeedConfigVhostList; then
|
||||
appError "Error retrieving vhost list: $error"
|
||||
return 1
|
||||
elif listContains "$domain" "$vhostList"; then
|
||||
appError "Domain already exists in OpenLiteSpeed config: $domain"
|
||||
return 1
|
||||
fi
|
||||
if ! run aliasList error openlitespeedConfigAliasList; then
|
||||
appError "Error retrieving alias list: $error"
|
||||
return 1
|
||||
elif listContains "$domain" "$aliasList"; then
|
||||
appError "Domain already exists in OpenLiteSpeed config (alias): $domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# MariaDB
|
||||
local databaseName databaseUser databaseNameList databaseUserList
|
||||
databaseName=$(siteConfigGetOrSet "$domain" "databaseName" "$(mariadbDomain2id "$domain")")
|
||||
databaseUser=$(siteConfigGetOrSet "$domain" "databaseUser" "$(mariadbDomain2id "$domain")")
|
||||
if ! run databaseNameList error mariadbDatabaseListGet; then
|
||||
appError "Error retrieving MariaDB database list: $error"
|
||||
return 1
|
||||
elif listContains "$databaseName" "$databaseNameList"; then
|
||||
appError "Database already exists in MariaDB: $databaseName"
|
||||
return 1
|
||||
fi
|
||||
if ! run databaseUserList error mariadbUserListGet; then
|
||||
appError "Error retrieving MariaDB user list: $error"
|
||||
return 1
|
||||
elif listContains "$databaseUser" "$databaseUserList"; then
|
||||
appError "User already exists in MariaDB: $databaseUser"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Redis
|
||||
local redisUser redisUserList
|
||||
redisUser=$(siteConfigGetOrSet "$domain" "redisUser" "$(redisDomain2id "$domain")")
|
||||
if ! run redisUserList error redisUserListGet; then
|
||||
appError "Error retrieving Redis user list: $error"
|
||||
return 1
|
||||
elif listContains "$redisUser" "$redisUserList"; then
|
||||
appError "User already exists in Redis: $redisUser"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Postfix
|
||||
# TODO
|
||||
# TODO Check site in Postfix
|
||||
# TODO
|
||||
|
||||
# Preparing and verifying the file source
|
||||
local tmpFiles=""
|
||||
local importFiles output rc
|
||||
if [[ -f "$sourceFiles" ]]; then
|
||||
tmpFiles=$(mktemp -d) || {
|
||||
appError "Failed to create temporary restore directory"
|
||||
return 1
|
||||
}
|
||||
|
||||
archiveExtract "$sourceFiles" "$tmpFiles" || {
|
||||
rm -rf -- "$tmpFiles" &>/dev/null
|
||||
return 1
|
||||
}
|
||||
|
||||
importFiles="$tmpFiles"
|
||||
else
|
||||
importFiles="$sourceFiles"
|
||||
fi
|
||||
|
||||
# Preparing and Verifying the Database Source
|
||||
if [[ -n "$sourceDatabase" ]]; then
|
||||
local importDbName importDbUser importDbPass
|
||||
importDbName=$(uuidgen) || return 1
|
||||
importDbUser=$(uuidgen) || return 1
|
||||
importDbPass=$(uuidgen) || return 1
|
||||
if ! runError error mariadbDatabaseUserSet "$importDbName" "$importDbUser" "$importDbPass"; then
|
||||
mariadbDatabaseRemove "$importDbName"
|
||||
mariadbUserRemove "$importDbUser"
|
||||
[[ -n "$tmpFiles" ]] && rm -rf -- "$tmpFiles" &>/dev/null
|
||||
appError "Failed to prepare tmp database"
|
||||
return 1
|
||||
fi
|
||||
if ! run output error mariadbMasterImport "$importDbName" "$importDbUser" "$importDbPass" "$sourceDatabase"; then
|
||||
mariadbDatabaseRemove "$importDbName"
|
||||
mariadbUserRemove "$importDbUser"
|
||||
[[ -n "$tmpFiles" ]] && rm -rf -- "$tmpFiles" &>/dev/null
|
||||
appError "Failed to import tmp database: ${error:-$output}"
|
||||
return 1
|
||||
fi
|
||||
mariadbDatabaseRemove "$importDbName"
|
||||
mariadbUserRemove "$importDbUser"
|
||||
fi
|
||||
|
||||
function _siteAddRollback() {
|
||||
local message="$1"
|
||||
|
||||
[[ -n "$tmpFiles" ]] && rm -rf -- "$tmpFiles" &>/dev/null
|
||||
|
||||
appError "$message | Rollback..."
|
||||
siteRemove "$domain" || true
|
||||
}
|
||||
|
||||
if ! runError error openlitespeedVhostRebuild "$domain"; then
|
||||
_siteAddRollback "Failed vhost rebuild (1): $error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! runError error cp -a "$importFiles/." "$targetPath/www/"; then
|
||||
_siteAddRollback "Failed copying files: $error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! runError error openlitespeedVhostRebuild "$domain"; then
|
||||
_siteAddRollback "Failed vhost rebuild (2): $error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! runError error openlitespeedVhostSet "$domain"; then
|
||||
_siteAddRollback "Failed OLS config rebuild: $error"
|
||||
return 1
|
||||
fi
|
||||
if ! runError error systemHostAdd "$domain"; then
|
||||
_siteAddRollback "Failed add domain to hosts: $error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local databasePass
|
||||
databasePass=$(siteConfigGetOrCreate "$domain" "databasePass")
|
||||
if ! runError error mariadbDatabaseUserSet "$databaseName" "$databaseUser" "$databasePass"; then
|
||||
_siteAddRollback "Failed create user and database in MariaDB: $error"
|
||||
return 1
|
||||
fi
|
||||
if [[ -n "$sourceDatabase" ]]; then
|
||||
if ! run output error mariadbMasterImport "$databaseName" "$databaseUser" "$databasePass" "$sourceDatabase"; then
|
||||
_siteAddRollback "Failed to import data in MariaDB: ${error:-$output}"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
local redisPass
|
||||
redisPass=$(siteConfigGetOrCreate "$domain" "redisPass")
|
||||
if ! runError error redisUserSet "$redisUser" "$redisPass"; then
|
||||
_siteAddRollback "Failed create user in Redis: $error"
|
||||
return 1
|
||||
fi
|
||||
if ! runError error redisUserListSave; then
|
||||
_siteAddRollback "Failed update user list: $error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! runError error postfixDomainSet "$domain"; then
|
||||
_siteAddRollback "Failed add domain in Postfix: $error"
|
||||
return 1
|
||||
fi
|
||||
# if ! runError error postfixPostmapRebuild; then
|
||||
# _siteAddRollback "Failed postmap rebuild: $error"
|
||||
# return 1
|
||||
# fi
|
||||
|
||||
printf '%s' "$domain"
|
||||
}
|
||||
|
||||
# Removes a site: deletes its DB, Redis user, Postfix domain, OLS config, files, and system user.
|
||||
# $1 (domain): site domain name.
|
||||
function siteRemove() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
if [[ -f "$appDataPath/config/$domain.config" ]]; then
|
||||
local databaseName databaseUser redisUser output error
|
||||
|
||||
databaseName=$(siteConfigGet "$domain" "databaseName")
|
||||
if [[ -n "$databaseName" ]]; then
|
||||
runSilent mariadbDatabaseRemove "$databaseName"
|
||||
fi
|
||||
databaseUser=$(siteConfigGet "$domain" "databaseUser")
|
||||
if [[ -n "$databaseUser" ]]; then
|
||||
runSilent mariadbUserRemove "$databaseUser"
|
||||
fi
|
||||
|
||||
redisUser=$(siteConfigGet "$domain" "redisUser")
|
||||
if [[ -n "$redisUser" ]]; then
|
||||
runSilent redisUserRemove "$redisUser"
|
||||
runSilent redisUserListSave
|
||||
fi
|
||||
fi
|
||||
|
||||
runSilent postfixDomainRemove "$domain"
|
||||
runSilent systemHostRemove "$domain"
|
||||
runSilent openlitespeedVhostConfigDel "$domain"
|
||||
|
||||
rm -f -- "$appDataPath/config/$domain.config" &>/dev/null
|
||||
[[ -n "$domain" && "$domain" != "/" ]] && rm -rf -- "$olsVhostsPath/$domain" &>/dev/null
|
||||
|
||||
local ug
|
||||
ug=$(domainToUser "$domain")
|
||||
if [[ -n "$ug" && "$ug" != "root" ]]; then
|
||||
if id "$ug" >/dev/null 2>&1; then
|
||||
userdel "$ug" &>/dev/null
|
||||
fi
|
||||
if getent group "$ug" >/dev/null 2>&1; then
|
||||
groupdel "$ug" &>/dev/null
|
||||
fi
|
||||
fi
|
||||
|
||||
printf '%s' "$domain"
|
||||
}
|
||||
|
||||
# Copies a site: exports source DB, creates target site, rebuilds WP config.
|
||||
# $1 (domain): source site domain name.
|
||||
# $2 (domainNew): target site domain name.
|
||||
function siteCopy() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local domainNew
|
||||
domainNew=$(domainPrepare "$2")
|
||||
domainCheck "$domainNew" || return 1
|
||||
|
||||
if [[ "$domain" == "$domainNew" ]]; then
|
||||
appError "The domains match: $domain -> $domainNew"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local vhostList
|
||||
if ! run vhostList error openlitespeedConfigVhostList; then
|
||||
appError "Error retrieving vhost list: $error"
|
||||
return 1
|
||||
elif ! listContains "$domain" "$vhostList"; then
|
||||
appError "$domain: Not found in vhosts list"
|
||||
return 1
|
||||
elif listContains "$domainNew" "$vhostList"; then
|
||||
appError "$domainNew: Found in vhosts list"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local aliasList
|
||||
if ! run aliasList error openlitespeedConfigAliasList; then
|
||||
appError "Error retrieving alias list: $error"
|
||||
return 1
|
||||
elif listContains "$domainNew" "$aliasList"; then
|
||||
appError "$domainNew: Found in alias list"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local databaseName databaseUser databasePass databaseFile
|
||||
databaseName=$(siteConfigGet "$domain" "databaseName")
|
||||
databaseUser=$(siteConfigGet "$domain" "databaseUser")
|
||||
databasePass=$(siteConfigGet "$domain" "databasePass")
|
||||
databaseFile=$(mktemp) || return 1
|
||||
|
||||
trap 'rm -f -- "$databaseFile"' RETURN
|
||||
|
||||
if ! runError error mariadbMasterExport "$databaseUser" "$databasePass" "$databaseName" "$databaseFile"; then
|
||||
appError "$domain: Failed database export: $error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! runError error siteAdd "$domainNew" "$olsVhostsPath/$domain/www" "$databaseFile"; then
|
||||
appError "$domainNew: Create site: $error"
|
||||
return 1
|
||||
fi
|
||||
rm -f -- "$databaseFile"
|
||||
|
||||
if ! runError error wordpressConfigRebuild "$domainNew"; then
|
||||
appError "$domainNew: Failed config rebuild: $error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! runError error wordpressDomainUpdate "$domainNew"; then
|
||||
appError "$domainNew: Failed config rebuild: $error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
trap - RETURN
|
||||
|
||||
printf '%s' "$domainNew"
|
||||
}
|
||||
|
||||
# Renames a site by copying it to the new domain and removing the old one.
|
||||
# $1 (domain): current site domain name.
|
||||
# $2 (domainNew): new site domain name.
|
||||
function siteRename() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local domainNew
|
||||
domainNew=$(domainPrepare "$2")
|
||||
domainCheck "$domainNew" || return 1
|
||||
|
||||
local error
|
||||
if ! runError error siteCopy "$domain" "$domainNew"; then
|
||||
appError "$error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local aliasList
|
||||
if run aliasList error openlitespeedConfigVhostAliasList "$domain" && [[ -n "$aliasList" ]]; then
|
||||
local -a aliases
|
||||
mapfile -t aliases <<< "$aliasList"
|
||||
# openlitespeedVhostAliasSet "$domain"
|
||||
# openlitespeedVhostAliasSet "$domainNew" "${aliases[@]}"
|
||||
openlitespeedVhostSet "$domain"
|
||||
openlitespeedVhostSet "$domainNew" "${aliases[@]}"
|
||||
fi
|
||||
|
||||
runSilent siteRemove "$domain" || true
|
||||
|
||||
printf '%s' "$domainNew"
|
||||
}
|
||||
|
||||
# Resets all service passwords for a site and rebuilds MariaDB, Redis, Postfix, and WordPress config.
|
||||
# $1 (domain): site domain name.
|
||||
function sitePasswordReset() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local error vhostList
|
||||
run vhostList error openlitespeedConfigVhostList || { appError "Failed get list of vhosts: $error"; return 1; }
|
||||
listContains "$domain" "$vhostList" || { appError "Domain is not exists in OpenLiteSpeed config"; return 1; }
|
||||
|
||||
local result=0
|
||||
|
||||
# reset passwords MariaDB
|
||||
local dbPass; dbPass=$(siteConfigGet "$domain" "databasePass")
|
||||
siteConfigSet "$domain" "databasePass"
|
||||
mariadbConfigRebuild "$domain" || { siteConfigSet "$domain" "databasePass" "$dbPass"; result=1; }
|
||||
|
||||
# reset passwords Redis
|
||||
local redisPass; redisPass=$(siteConfigGet "$domain" "redisPass")
|
||||
siteConfigSet "$domain" "redisPass"
|
||||
redisConfigRebuild "$domain" || { siteConfigSet "$domain" "redisPass" "$redisPass"; result=1; }
|
||||
|
||||
# reset passwords Postfix
|
||||
local postfixPass; postfixPass=$(siteConfigGet "$domain" "postfixPass")
|
||||
siteConfigSet "$domain" "postfixPass"
|
||||
postfixConfigRebuild "$domain" || { siteConfigSet "$domain" "postfixPass" "$postfixPass"; result=1; }
|
||||
|
||||
# update Wordpress config
|
||||
wordpressConfigRebuild "$domain" || result=1
|
||||
return $result
|
||||
}
|
||||
|
||||
# Resets all service credentials (passwords and usernames) and rebuilds all service configs for a site.
|
||||
# $1 (domain): site domain name.
|
||||
function siteAuthReset() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local error vhostList
|
||||
run vhostList error openlitespeedConfigVhostList || { appError "Failed get list of vhosts: $error"; return 1; }
|
||||
listContains "$domain" "$vhostList" || { appError "Domain is not exists in OpenLiteSpeed config"; return 1; }
|
||||
|
||||
local result=0
|
||||
|
||||
# reset passwords MariaDB
|
||||
local dbPass dbUser
|
||||
dbPass=$(siteConfigGet "$domain" "databasePass")
|
||||
dbUser=$(siteConfigGet "$domain" "databaseUser")
|
||||
siteConfigSet "$domain" "databasePass"
|
||||
siteConfigSet "$domain" "databaseUser"
|
||||
mariadbConfigRebuild "$domain" || { siteConfigSet "$domain" "databasePass" "$dbPass"; siteConfigSet "$domain" "databaseUser" "$dbUser"; result=1; }
|
||||
|
||||
# reset passwords Redis
|
||||
local redisPass redisUser
|
||||
redisPass=$(siteConfigGet "$domain" "redisPass")
|
||||
redisUser=$(siteConfigGet "$domain" "redisUser")
|
||||
siteConfigSet "$domain" "redisPass"
|
||||
siteConfigSet "$domain" "redisUser"
|
||||
redisConfigRebuild "$domain" || { siteConfigSet "$domain" "redisPass" "$redisPass"; siteConfigSet "$domain" "redisUser" "$redisUser"; result=1; }
|
||||
|
||||
# reset passwords Postfix
|
||||
local postfixPass postfixUser
|
||||
postfixPass=$(siteConfigGet "$domain" "postfixPass")
|
||||
postfixUser=$(siteConfigGet "$domain" "postfixUser")
|
||||
siteConfigSet "$domain" "postfixPass"
|
||||
siteConfigSet "$domain" "postfixUser"
|
||||
postfixConfigRebuild "$domain" || { siteConfigSet "$domain" "postfixPass" "$postfixPass"; siteConfigSet "$domain" "postfixUser" "$postfixUser"; result=1; }
|
||||
|
||||
# update Wordpress config
|
||||
wordpressConfigRebuild "$domain" || result=1
|
||||
return $result
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
# Updates APT packages and installs required system dependencies.
|
||||
function systemApt() {
|
||||
apt update && apt upgrade -y || return 1
|
||||
apt install -y curl ipset iptables-persistent ipset-persistent jq zip mc nano idn2 acl xfsprogs opendkim-tools pigz
|
||||
}
|
||||
|
||||
# Creates ipset sets for WEDOS, WEDOS Global, and whitelist traffic, and installs an hourly update cron job.
|
||||
function systemIpset() {
|
||||
ipset list wedos &>/dev/null || ipset create wedos hash:ip family inet || {
|
||||
appError "Failed create ipset: wedos"
|
||||
return 1
|
||||
}
|
||||
ipset list wedos6 &>/dev/null || ipset create wedos6 hash:ip family inet6 || {
|
||||
appError "Failed create ipset: wedos6"
|
||||
return 1
|
||||
}
|
||||
ipset list wedos-global &>/dev/null || ipset create wedos-global hash:net family inet || {
|
||||
appError "Failed create ipset: wedos-global"
|
||||
return 1
|
||||
}
|
||||
ipset list wedos-global6 &>/dev/null || ipset create wedos-global6 hash:net family inet6 || {
|
||||
appError "Failed create ipset: wedos-global6"
|
||||
return 1
|
||||
}
|
||||
ipset list whitelist &>/dev/null || ipset create whitelist hash:ip family inet || {
|
||||
appError "Failed create ipset: whitelist"
|
||||
return 1
|
||||
}
|
||||
ipset list whitelist6 &>/dev/null || ipset create whitelist6 hash:ip family inet6 || {
|
||||
appError "Failed create ipset: whitelist6"
|
||||
return 1
|
||||
}
|
||||
|
||||
ipset add wedos 46.28.104.66 -exist
|
||||
ipset add wedos 46.28.107.200 -exist
|
||||
ipset add wedos 46.28.104.146 -exist
|
||||
ipset add wedos 46.28.107.215 -exist
|
||||
|
||||
local cron="/etc/cron.d/wedos-global-update"
|
||||
local job='0 * * * * root curl -fsSL https://ips.wedos.global/ips.json | jq -r '"'"'.list[]'"'"' | while read -r ip; do [[ "$ip" == *:* ]] && ipset add wedos-global6 "$ip" -exist || ipset add wedos-global "$ip" -exist; done >> /var/log/wedos-ipset-update.log 2>&1'
|
||||
printf '%s\n' "$job" > "$cron" || {
|
||||
appError "Failed write cron file: $cron"
|
||||
return 1
|
||||
}
|
||||
chmod 644 "$cron" || {
|
||||
appError "Failed chmod cron file: $cron"
|
||||
return 1
|
||||
}
|
||||
|
||||
(set -o pipefail; curl -fsSL https://ips.wedos.global/ips.json | jq -r '.list[]' | while read -r ip; do
|
||||
[[ "$ip" == *:* ]] && ipset add wedos-global6 "$ip" -exist || ipset add wedos-global "$ip" -exist
|
||||
done) >> /var/log/wedos-ipset-update.log 2>&1 || appError "Failed to update WEDOS Global IP sets."
|
||||
}
|
||||
|
||||
# Installs persistent iptables and ip6tables INPUT rules, then saves and reloads via netfilter-persistent.
|
||||
function systemIptables() {
|
||||
iptables -C INPUT -m set --match-set wedos src -j ACCEPT 2>/dev/null || iptables -I INPUT 1 -m set --match-set wedos src -j ACCEPT
|
||||
ip6tables -C INPUT -m set --match-set wedos6 src -j ACCEPT 2>/dev/null || ip6tables -I INPUT 1 -m set --match-set wedos6 src -j ACCEPT
|
||||
|
||||
iptables -C INPUT -m set --match-set wedos-global src -p tcp -m multiport --dports 80,443 -j ACCEPT 2>/dev/null || \
|
||||
iptables -I INPUT 2 -m set --match-set wedos-global src -p tcp -m multiport --dports 80,443 -j ACCEPT
|
||||
ip6tables -C INPUT -m set --match-set wedos-global6 src -p tcp -m multiport --dports 80,443 -j ACCEPT 2>/dev/null || \
|
||||
ip6tables -I INPUT 2 -m set --match-set wedos-global6 src -p tcp -m multiport --dports 80,443 -j ACCEPT
|
||||
|
||||
iptables -C INPUT -m set --match-set whitelist src -p tcp -m multiport --dports 80,443 -j ACCEPT 2>/dev/null || \
|
||||
iptables -I INPUT 3 -m set --match-set whitelist src -p tcp -m multiport --dports 80,443 -j ACCEPT
|
||||
ip6tables -C INPUT -m set --match-set whitelist6 src -p tcp -m multiport --dports 80,443 -j ACCEPT 2>/dev/null || \
|
||||
ip6tables -I INPUT 3 -m set --match-set whitelist6 src -p tcp -m multiport --dports 80,443 -j ACCEPT
|
||||
|
||||
iptables -C INPUT -i lo -j ACCEPT 2>/dev/null || iptables -I INPUT 4 -i lo -j ACCEPT
|
||||
ip6tables -C INPUT -i lo -j ACCEPT 2>/dev/null || ip6tables -I INPUT 4 -i lo -j ACCEPT
|
||||
|
||||
iptables -C INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT 2>/dev/null || \
|
||||
iptables -I INPUT 5 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
|
||||
ip6tables -C INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT 2>/dev/null || \
|
||||
ip6tables -I INPUT 5 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
|
||||
|
||||
iptables -C INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT 2>/dev/null || \
|
||||
iptables -I INPUT 6 -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT
|
||||
ip6tables -C INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT 2>/dev/null || \
|
||||
ip6tables -I INPUT 6 -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT
|
||||
|
||||
iptables -C INPUT -j DROP 2>/dev/null || iptables -A INPUT -j DROP
|
||||
ip6tables -C INPUT -j DROP 2>/dev/null || ip6tables -A INPUT -j DROP
|
||||
|
||||
netfilter-persistent save || return 1
|
||||
netfilter-persistent reload || return 1
|
||||
}
|
||||
|
||||
# Returns non-empty root crontab lines; empty result if no crontab exists.
|
||||
function systemCronList() {
|
||||
local result
|
||||
if result="$(crontab -u root -l 2>&1)"; then
|
||||
:
|
||||
elif grep -qi 'no crontab for' <<< "$result"; then
|
||||
result=""
|
||||
else
|
||||
appError "Failed to retrieve cron jobs: $result"
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf '%s\n' "$result" | awk 'NF'
|
||||
}
|
||||
|
||||
# Returns the IP address for a domain from /etc/hosts.
|
||||
# $1 (domain): domain name to look up.
|
||||
function systemHostGet() {
|
||||
local domain="$1"
|
||||
[[ -n "$domain" ]] || { appError "Domain not specified"; return 1; }
|
||||
|
||||
awk -v domain="$domain" '
|
||||
$1 !~ /^#/ {
|
||||
for (i = 2; i <= NF; i++) {
|
||||
if ($i == domain) {
|
||||
print $1
|
||||
exit
|
||||
}
|
||||
}
|
||||
}
|
||||
' /etc/hosts
|
||||
}
|
||||
|
||||
# Adds a domain entry to /etc/hosts if not already present.
|
||||
# $1 (domain): site domain name.
|
||||
# [$2] (ip): IP address (defaults to 127.0.0.1).
|
||||
function systemHostAdd() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local ip="${2:-127.0.0.1}"
|
||||
|
||||
if ! awk -v ip="$ip" -v domain="$domain" '$1 == ip && $2 == domain { found=1 } END { exit !found }' /etc/hosts; then
|
||||
printf '%s %s\n' "$ip" "$domain" >> /etc/hosts || {
|
||||
appError "Failed writing hosts"
|
||||
return 1
|
||||
}
|
||||
fi
|
||||
}
|
||||
|
||||
# Removes a domain entry from /etc/hosts.
|
||||
# $1 (domain): site domain name.
|
||||
# [$2] (ip): IP address (defaults to 127.0.0.1).
|
||||
function systemHostRemove() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local ip="${2:-127.0.0.1}"
|
||||
|
||||
local tmp
|
||||
tmp=$(mktemp) || return 1
|
||||
|
||||
if ! awk -v ip="$ip" -v domain="$domain" '!($1 == ip && $2 == domain)' /etc/hosts > "$tmp"; then
|
||||
rm -f "$tmp"
|
||||
appError "Failed editing hosts"
|
||||
return 1
|
||||
fi
|
||||
|
||||
mv -- "$tmp" /etc/hosts || {
|
||||
rm -f "$tmp"
|
||||
appError "Failed writing hosts"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
# Lists users in the SFTP access group.
|
||||
function sftpUserList() {
|
||||
getent group "$sftpAccessGroup" | awk -F: '{print $4}' | tr ',' '\n' | sed '/^$/d' | sort
|
||||
}
|
||||
|
||||
# Sets the SFTP password for a domain user from site config.
|
||||
function sftpPasswordSet() {
|
||||
local domain="$1"
|
||||
domain=$(domainPrepare "$domain")
|
||||
domainCheck "$domain" || return 1
|
||||
[[ -n "$domain" && "$domain" != "root" ]] || { appError "Incorrect or empty domain: $domain"; return 1; }
|
||||
|
||||
local ug sftpPass
|
||||
ug=$(domainToUser "$domain")
|
||||
|
||||
id "$ug" >/dev/null 2>&1 || { appError "User does not exist: $ug"; return 1; }
|
||||
sftpPass=$(siteConfigGetOrCreate "$domain" "sftpPass")
|
||||
[[ -n "$sftpPass" ]] || { appError "SFTP password is empty for domain: $domain"; return 1; }
|
||||
|
||||
printf '%s:%s\n' "$ug" "$sftpPass" | chpasswd || { appError "Change SFTP password failed for user: $ug"; return 1; }
|
||||
}
|
||||
|
||||
# Enables SFTP access for a domain user.
|
||||
function sftpAccessEnable() {
|
||||
local domain="$1"
|
||||
domain=$(domainPrepare "$domain")
|
||||
domainCheck "$domain" || return 1
|
||||
[[ -n "$domain" && "$domain" != "root" ]] || { appError "Incorrect or empty domain: $domain"; return 1; }
|
||||
|
||||
local output error ug
|
||||
ug=$(domainToUser "$domain")
|
||||
id "$ug" >/dev/null 2>&1 || { appError "User does not exist: $ug"; return 1; }
|
||||
[[ -d "$olsVhostsPath/$domain/www" ]] || { appError "Vhost www directory does not exist: $olsVhostsPath/$domain/www"; return 1; }
|
||||
|
||||
if ! id -nG "$ug" | tr ' ' '\n' | grep -Fxq "$sftpAccessGroup"; then
|
||||
run output error usermod -aG "$sftpAccessGroup" "$ug" || { appError "Add user $ug to $sftpAccessGroup: $error"; return 1; }
|
||||
fi
|
||||
|
||||
sftpPasswordSet "$domain"
|
||||
}
|
||||
|
||||
# Disables SFTP access for a domain user by removing them from the SFTP group.
|
||||
function sftpAccessDisable() {
|
||||
local domain="$1"
|
||||
domain=$(domainPrepare "$domain")
|
||||
domainCheck "$domain" || return 1
|
||||
[[ -n "$domain" && "$domain" != "root" ]] || { appError "Incorrect or empty domain: $domain"; return 1; }
|
||||
|
||||
local output error ug
|
||||
ug=$(domainToUser "$domain")
|
||||
id "$ug" >/dev/null 2>&1 || { appError "User does not exist: $ug"; return 1; }
|
||||
|
||||
if id -nG "$ug" | tr ' ' '\n' | grep -Fxq "$sftpAccessGroup"; then
|
||||
run output error gpasswd -d "$ug" "$sftpAccessGroup" || { appError "Remove user $ug from $sftpAccessGroup: $error"; return 1; }
|
||||
fi
|
||||
}
|
||||
|
||||
# [WARNING] Patches sshd_config to enable SFTP via internal-sftp with group-based chroot.
|
||||
function sftpAddingSupport() {
|
||||
local sftpConfig="/etc/ssh/sshd_config"
|
||||
local sshService sshdBin sftpBackup output error
|
||||
|
||||
# printInfo "$systemLabel SFTP Adding support for SFTP access"
|
||||
[[ -f "$sftpConfig" ]] || { appError "SFTP Config not found: $sftpConfig"; return 1; }
|
||||
|
||||
if systemctl list-unit-files | grep -q '^sshd\.service'; then
|
||||
sshService="sshd"
|
||||
elif systemctl list-unit-files | grep -q '^ssh\.service'; then
|
||||
sshService="ssh"
|
||||
else
|
||||
appError "SFTP Service not found"
|
||||
return 1
|
||||
fi
|
||||
# printInfo "$systemLabel SFTP Use service: $sshService"
|
||||
|
||||
sshdBin="$(command -v sshd || true)"
|
||||
[[ -n "$sshdBin" ]] || { appError "SFTP Binary not found"; return 1; }
|
||||
|
||||
if ! getent group "$sftpAccessGroup" >/dev/null 2>&1; then
|
||||
# printInfo "$systemLabel SFTP Create SFTP access group: $sftpAccessGroup"
|
||||
run output error groupadd "$sftpAccessGroup" || { appError "SFTP Failed create group $sftpAccessGroup: $error"; return 1; }
|
||||
fi
|
||||
|
||||
grep -Eq '^[[:space:]]*Subsystem[[:space:]]+sftp[[:space:]]+' "$sftpConfig" || {
|
||||
appError "SFTP Not found Subsystem in $sftpConfig"
|
||||
return 1
|
||||
}
|
||||
|
||||
sftpBackup="$sftpConfig.$(date +%F_%H-%M-%S).bak"
|
||||
cp -a "$sftpConfig" "$sftpBackup" || { appError "SFTP Backup failed"; return 1; }
|
||||
|
||||
# printInfo "$systemLabel SFTP Update config..."
|
||||
if ! sed -i -E 's|^[[:space:]]*Subsystem[[:space:]]+sftp[[:space:]]+.*$|Subsystem sftp internal-sftp|' "$sftpConfig"; then
|
||||
cp -a "$sftpBackup" "$sftpConfig"
|
||||
appError "SFTP Update Subsystem SFTP failed"
|
||||
return 1
|
||||
fi
|
||||
if ! sed -i \
|
||||
-e '/^# --- KUBE SFTP GLOBAL BEGIN ---$/,/^# --- KUBE SFTP GLOBAL END ---$/d' \
|
||||
-e '/^# --- KUBE SFTP GROUP BEGIN ---$/,/^# --- KUBE SFTP GROUP END ---$/d' \
|
||||
"$sftpConfig"; then
|
||||
cp -a "$sftpBackup" "$sftpConfig"
|
||||
appError "SFTP Remove old SFTP blocks failed"
|
||||
return 1
|
||||
fi
|
||||
local tmpFile
|
||||
tmpFile="$(mktemp)"
|
||||
if ! {
|
||||
cat "$appAssetsPath/system/sftp-global.conf"
|
||||
echo
|
||||
cat "$sftpConfig"
|
||||
echo
|
||||
sed "s|{{sftp_access_group}}|$sftpAccessGroup|g" "$appAssetsPath/system/sftp-group.conf"
|
||||
} > "$tmpFile" || ! mv "$tmpFile" "$sftpConfig"; then
|
||||
rm -f "$tmpFile"
|
||||
cp -a "$sftpBackup" "$sftpConfig"
|
||||
appError "SFTP Append SFTP config blocks failed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# printInfo "$systemLabel SFTP Config validation..."
|
||||
if ! run output error "$sshdBin" -t -f "$sftpConfig"; then
|
||||
cp -a "$sftpBackup" "$sftpConfig"
|
||||
appError "SFTP Config validation failed: $error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# printInfo "$systemLabel SFTP Reload/restart service..."
|
||||
if ! run output error systemctl reload "$sshService"; then
|
||||
if ! run output error systemctl restart "$sshService"; then
|
||||
cp -a "$sftpBackup" "$sftpConfig"
|
||||
systemctl restart "$sshService" >/dev/null 2>&1 || true
|
||||
appError "SFTP Reload/restart failed: $error"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# printSuccess "$systemLabel SFTP Adding support complete"
|
||||
}
|
||||
|
||||
function fail2banConfigUpdate() {
|
||||
local sourceConfig targetConfig
|
||||
targetConfig="/etc/fail2ban/jail.local"
|
||||
sourceConfig="$appAssetsPath/system/fail2ban.conf"
|
||||
|
||||
[[ ! -f "$targetConfig" ]] || {
|
||||
appError "The file $targetConfig already exists. Make changes manually.";
|
||||
return 1;
|
||||
}
|
||||
|
||||
cp -a "$sourceConfig" "$targetConfig" >/dev/null 2>&1 || {
|
||||
appError "File copy error";
|
||||
return 1;
|
||||
}
|
||||
|
||||
if command -v fail2ban-client >/dev/null 2>&1; then
|
||||
fail2ban-client -t >/dev/null 2>&1 || {
|
||||
appError "Configuration error";
|
||||
return 1;
|
||||
}
|
||||
fi
|
||||
|
||||
systemctl restart fail2ban
|
||||
sleep 2
|
||||
systemctl is-active --quiet fail2ban || {
|
||||
appError "fail2ban did not start after applying the new configuration";
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
function unigmaGetTxt() {
|
||||
local domain="$1"
|
||||
local raw
|
||||
|
||||
raw=$(dig +short +time=2 +tries=1 TXT "$domain.settings.wedos-int.dev" 2>/dev/null)
|
||||
[[ -n "$raw" ]] || return 1
|
||||
|
||||
printf '%s\n' "$raw" | tr -d '"' | tr '\n' ' '
|
||||
}
|
||||
|
||||
function unigmaParse() {
|
||||
local raw="$1"
|
||||
UNIGMA_PHP="" UNIGMA_MEM="" UNIGMA_EXEC=""
|
||||
|
||||
local IFS=';' pair key value
|
||||
for pair in $raw; do
|
||||
pair="$(sed 's/^[[:space:]]*//;s/[[:space:]]*$//' <<< "$pair")"
|
||||
[[ -z "$pair" || "$pair" != *"="* ]] && continue
|
||||
|
||||
key="$(sed 's/^[[:space:]]*//;s/[[:space:]]*$//' <<< "${pair%%=*}")"
|
||||
value="$(sed 's/^[[:space:]]*//;s/[[:space:]]*$//' <<< "${pair#*=}")"
|
||||
|
||||
case "$key" in
|
||||
php) UNIGMA_PHP="$value" ;;
|
||||
mem|memory_limit) UNIGMA_MEM="$value" ;;
|
||||
exec|max_execution_time) UNIGMA_EXEC="$value" ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
function unigmaIniSet() {
|
||||
local domain="$1"
|
||||
local mem="$2"
|
||||
local exec="$3"
|
||||
local file content="" existing uid gid
|
||||
|
||||
[[ -n "$mem" || -n "$exec" ]] || return 0
|
||||
|
||||
uid=$(domainToUid "$domain")
|
||||
[[ -n "$uid" ]] || { appError "Cannot resolve UID for: $domain"; return 1; }
|
||||
gid=$(domainToUid "$domain")
|
||||
[[ -n "$gid" ]] || { appError "Cannot resolve GID for: $domain"; return 1; }
|
||||
|
||||
[[ -n "$mem" ]] && content+="memory_limit = ${mem}"$'\n'
|
||||
[[ -n "$exec" ]] && content+="max_execution_time = ${exec}"$'\n'
|
||||
file="$olsPrivatePath/$domain/php/01-ols-private.ini"
|
||||
|
||||
# no change
|
||||
existing="$(cat "$file" 2>/dev/null || true)"
|
||||
[[ "${content%$'\n'}" == "$existing" ]] && return 0
|
||||
|
||||
fileAtomicWrite "$file" "$content"
|
||||
chown -- "$uid:$gid" "$file" || { appError "Change owner of file: $file"; return 1; }
|
||||
return 2
|
||||
}
|
||||
|
||||
function unigmaPhpSet() {
|
||||
local domain="$1"
|
||||
local php="$2"
|
||||
local path="$olsVhostsPath/$domain/www"
|
||||
[[ -d "$path" ]] || { appError "Directory not found: $path"; return 1; }
|
||||
|
||||
local uid gid
|
||||
uid=$(domainToUid "$domain")
|
||||
[[ -n "$uid" ]] || { appError "Cannot resolve UID for: $domain"; return 1; }
|
||||
gid=$(domainToGid "$domain")
|
||||
[[ -n "$gid" ]] || { appError "Cannot resolve GID for: $domain"; return 1; }
|
||||
|
||||
if [[ -z "$php" ]]; then
|
||||
find "$path" -maxdepth 1 -type f -name '.php[1-9][0-9]' -delete
|
||||
return 0
|
||||
fi
|
||||
|
||||
local suffix="${php//./}"
|
||||
[[ "$suffix" =~ ^[1-9][0-9]$ ]] || { appError "Incorrect PHP version: $php"; return 1; }
|
||||
|
||||
# target
|
||||
local target="$path/.php$suffix"
|
||||
|
||||
# find files .phpXX
|
||||
local -a current
|
||||
mapfile -t current < <(find "$path" -maxdepth 1 -type f -name '.php[1-9][0-9]')
|
||||
if [[ ${#current[@]} -eq 0 ]]; then
|
||||
touch "$target"
|
||||
chown -- "$uid:$gid" "$target" || true
|
||||
return 0
|
||||
fi
|
||||
|
||||
# no change
|
||||
if [[ ${#current[@]} -eq 1 && "${current[0]}" == "$target" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# rename (atomic substitution)
|
||||
mv -f "${current[0]}" "$target"
|
||||
chown -- "$uid:$gid" "$target" || true
|
||||
|
||||
# remove other files .phpXX
|
||||
if [[ ${#current[@]} -gt 1 ]]; then
|
||||
local i
|
||||
for ((i = 1; i < ${#current[@]}; i++)); do
|
||||
rm -f "${current[i]}"
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
function unigmaUpdate() {
|
||||
local domain="$1"
|
||||
|
||||
local raw
|
||||
raw="$(unigmaGetTxt "$domain")" || return 0
|
||||
|
||||
unigmaParse "$raw"
|
||||
unigmaIniSet "$domain" "$UNIGMA_MEM" "$UNIGMA_EXEC"
|
||||
unigmaPhpSet "$domain" "$UNIGMA_PHP"
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
# Lists WordPress users via WP-CLI.
|
||||
# $1 (domain): site domain name.
|
||||
function wordpressUserList() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
wordpressExec "$domain" user list
|
||||
}
|
||||
|
||||
# Sets a WordPress user's password via WP-CLI.
|
||||
# $1 (domain): site domain name.
|
||||
# $2 (user): WordPress username or ID.
|
||||
# $3 (pass): new password.
|
||||
function wordpressPasswordSet() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local user="$2"
|
||||
[[ -n "$user" ]] || { appError "User not specified"; return 1; }
|
||||
|
||||
local pass="$3"
|
||||
[[ -n "$pass" ]] || { appError "Password not specified"; return 1; }
|
||||
|
||||
wordpressExec "$domain" user update "$user" --user_pass="$pass"
|
||||
}
|
||||
|
||||
# Runs WP-CLI inside the OLS pod for the specified domain.
|
||||
# $1 (domain): site domain name.
|
||||
# $2+ (...): WP-CLI arguments.
|
||||
function wordpressExec() {
|
||||
local domain="$1"
|
||||
[[ -n "$domain" ]] || { appError "Domain not specified"; return 1; }
|
||||
domain=$(domainPrepare "$domain")
|
||||
shift || true
|
||||
|
||||
openlitespeedExec wp --path="$olsPodVhostsPath/$domain/www" --allow-root --skip-plugins --skip-themes --require="$olsPodPrivatePath/$domain/bootstrap.php" --exec='error_reporting(0);define("WP_DEBUG",false);' "$@"
|
||||
}
|
||||
|
||||
function wordpressSalt() {
|
||||
local domain="$1"
|
||||
[[ -n "$domain" ]] || { appError "Domain not specified"; return 1; }
|
||||
domain=$(domainPrepare "$domain")
|
||||
shift || true
|
||||
|
||||
wordpressExec "$domain" config shuffle-salts;
|
||||
}
|
||||
|
||||
# Replaces a string in the WordPress database via WP-CLI search-replace.
|
||||
# $1 (domain): site domain name.
|
||||
# $2 (search): string to search for.
|
||||
# $3 (replace): replacement string.
|
||||
function wordpressSearchReplace() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local search="$2"
|
||||
[[ -n "$search" ]] || { appError "Search string not specified"; return 1; }
|
||||
|
||||
local replace="$3"
|
||||
[[ -n "$replace" ]] || { appError "Replacement string not specified"; return 1; }
|
||||
|
||||
[[ "$search" == "$replace" ]] && return 0
|
||||
|
||||
wordpressExec "$domain" search-replace "$search" "$replace" --all-tables-with-prefix --precise --skip-columns=guid --report-changed-only
|
||||
}
|
||||
|
||||
# Writes the bootstrap.php configuration file for a site.
|
||||
# $1 (domain): site domain name.
|
||||
function wordpressConfigUpdate() {
|
||||
local domain="$1"
|
||||
domain=$(domainPrepare "$domain")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local ug bootstrapFile tmpFile
|
||||
ug=$(domainToUser "$domain")
|
||||
bootstrapFile="$olsPrivatePath/$domain/bootstrap.php"
|
||||
tmpFile="$bootstrapFile".tmp
|
||||
|
||||
local databaseName databaseUser databasePass redisUser redisPass postfixUser postfixPass key
|
||||
databaseName="$(siteConfigGet "$domain" "databaseName")"
|
||||
databaseUser="$(siteConfigGet "$domain" "databaseUser")"
|
||||
databasePass="$(siteConfigGet "$domain" "databasePass")"
|
||||
redisUser="$(siteConfigGet "$domain" "redisUser")"
|
||||
redisPass="$(siteConfigGet "$domain" "redisPass")"
|
||||
postfixUser="$(siteConfigGet "$domain" "postfixUser")"
|
||||
postfixPass="$(siteConfigGet "$domain" "postfixPass")"
|
||||
for key in databaseName databaseUser databasePass redisUser redisPass postfixUser postfixPass; do
|
||||
[[ -n "${!key}" ]] || { appError "wordpressConfigUpdate: $key is empty"; return 1; }
|
||||
done
|
||||
|
||||
# aliases...
|
||||
local aliasList aliasFirst error
|
||||
run aliasList error openlitespeedConfigVhostAliasList "$domain"
|
||||
aliasFirst=$(awk 'NR==1' <<< "$aliasList")
|
||||
|
||||
cat > "$tmpFile" <<PHP
|
||||
<?php
|
||||
|
||||
define('DB_HOST', '$mariadbMasterKube');
|
||||
define('DB_NAME', '$databaseName');
|
||||
define('DB_USER', '$databaseUser');
|
||||
define('DB_PASSWORD', '$databasePass');
|
||||
|
||||
define('WP_REDIS_SELECTIVE_FLUSH', true);
|
||||
define('WP_REDIS_PASSWORD', ['$redisUser', '$redisPass']);
|
||||
define('WP_REDIS_PREFIX', '${redisUser}:');
|
||||
define('WP_REDIS_PORT', 6379);
|
||||
define('WP_REDIS_HOST', '${redisKube}-master');
|
||||
define('WP_REDIS_CLIENT', 'phpredis');
|
||||
|
||||
define('SODEW_SMTP_USER', '${postfixUser}@${postfixDefaultRealm}');
|
||||
define('SODEW_SMTP_PASS', '$postfixPass');
|
||||
define('SODEW_SMTP_HOST', '$postfixHost');
|
||||
define('SODEW_SMTP_POSTMASTER', '$postfixPostmaster');
|
||||
|
||||
define('HOSTING_WP_DOMAIN', "$domain");
|
||||
PHP
|
||||
|
||||
# adding a constant for the alias — if it exists
|
||||
[[ -n "$aliasFirst" ]] && printf "define('HOSTING_WP_INTERNAL_URL', '%s');\n" "$aliasFirst" >> "$tmpFile"
|
||||
|
||||
chown -R -- "$ug:$ug" "$tmpFile" || { appError "Change owner of vhost data directory failed: $tmpFile"; return 1; }
|
||||
chmod 600 -- "$tmpFile" || { appError "Change permissions of vhost data files failed: $tmpFile"; return 1; }
|
||||
mv -f -- "$tmpFile" "$bootstrapFile" || return 1
|
||||
}
|
||||
|
||||
# Wraps WordPress define() calls to be conditional (defined() || define(...)).
|
||||
# $1 (domain): site domain name.
|
||||
function wordpressConfigDefine() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local configFile="$olsVhostsPath/$domain/www/wp-config.php"
|
||||
[[ -f "$configFile" ]] || { appError "File not found: $configFile"; return 1; }
|
||||
|
||||
local constants=(
|
||||
DB_NAME
|
||||
DB_USER
|
||||
DB_PASSWORD
|
||||
DB_HOST
|
||||
DB_CHARSET
|
||||
DB_COLLATE
|
||||
WP_REDIS_SELECTIVE_FLUSH
|
||||
WP_REDIS_PASSWORD
|
||||
WP_REDIS_PREFIX
|
||||
WP_REDIS_PORT
|
||||
WP_REDIS_HOST
|
||||
WP_REDIS_CLIENT
|
||||
SODEW_SMTP_USER
|
||||
SODEW_SMTP_PASS
|
||||
SODEW_SMTP_HOST
|
||||
SODEW_SMTP_POSTMASTER
|
||||
)
|
||||
|
||||
local const
|
||||
for const in "${constants[@]}"; do
|
||||
sed -i -E "s@^([[:space:]]*)(define[[:space:]]*\([[:space:]]*['\"]${const}['\"][[:space:]]*,)@\1defined('${const}') || \2@" "$configFile"
|
||||
done
|
||||
}
|
||||
|
||||
# Rebuilds WordPress configuration for a site: defines, bootstrap, and mu-plugins.
|
||||
# $1 (domain): site domain name.
|
||||
function wordpressConfigRebuild() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
wordpressConfigDefine "$domain" || return 1
|
||||
wordpressConfigUpdate "$domain" || return 1
|
||||
|
||||
local muPluginsPath ug
|
||||
muPluginsPath="$olsVhostsPath/$domain/www/wp-content/mu-plugins"
|
||||
ug=$(domainToUser "$domain")
|
||||
|
||||
[[ -d "$muPluginsPath" ]] || mkdir -p -- "$muPluginsPath" || { appError "Create directory failed: $muPluginsPath"; return 1; }
|
||||
cp -f -- "$appAssetsPath/wordpress/shared/00-hosting-loader.php" "$muPluginsPath/00-hosting-loader.php" || { appError "Copy file failed: $muPluginsPath/00-hosting-loader.php"; return 1; }
|
||||
chown -R -- "$ug:$ug" "$muPluginsPath" || { appError "Change owner of directory failed: $muPluginsPath"; return 1; }
|
||||
chmod 2750 "$muPluginsPath" || { appError "Change permissions of directories failed: $muPluginsPath"; return 1; }
|
||||
chmod 0640 "$muPluginsPath/00-hosting-loader.php" || { appError "Change permissions of file failed: $muPluginsPath/00-hosting-loader.php"; return 1; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
# Placeholder for extended WordPress config rebuild (not yet implemented).
|
||||
# $1 (domain): site domain name.
|
||||
function wordpressConfigRebuildEx() {
|
||||
echo '*****'
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Updates all WordPress URLs in DB to match the current domain, cleans cache and Redis.
|
||||
# $1 (domain): site domain name.
|
||||
# [$2] (abspathOld): old absolute path to replace.
|
||||
function wordpressDomainUpdate() {
|
||||
local domain="$1"
|
||||
[[ -n "$domain" ]] || { printDanger "$wordpressLabel Domain not specified"; return 1; }
|
||||
local siteNew="https://$domain"
|
||||
|
||||
local abspathOld="$2"
|
||||
|
||||
local isMultiSite
|
||||
isMultiSite=$(wordpressExec "$domain" eval 'echo is_multisite() ? 1 : 0;')
|
||||
if [[ "$isMultiSite" == "1" ]]; then
|
||||
printDanger "$wordpressLabel This script is for SINGLE SITE only. Detected multisite. Abort."
|
||||
return 1
|
||||
fi
|
||||
|
||||
local siteConst homeConst siteOption homeOption
|
||||
siteConst=$(wordpressExec "$domain" eval 'echo defined("WP_SITEURL")?WP_SITEURL:"";' || true)
|
||||
siteConst=$(strTrimSlash "$siteConst")
|
||||
homeConst=$(wordpressExec "$domain" eval 'echo defined("WP_HOME")?WP_HOME:"";' || true)
|
||||
homeConst=$(strTrimSlash "$homeConst")
|
||||
siteOption=$(wordpressExec "$domain" option get siteurl 2>/dev/null || true)
|
||||
siteOption=$(strTrimSlash "$siteOption")
|
||||
homeOption=$(wordpressExec "$domain" option get home 2>/dev/null || true)
|
||||
homeOption=$(strTrimSlash "$homeOption")
|
||||
printInfo "$wordpressLabel Wordpress siteurl | Const: $siteConst | Option: $siteOption"
|
||||
printInfo "$wordpressLabel Wordpress home | Const: $homeConst | Option: $homeOption"
|
||||
|
||||
local siteOld="${siteConst:-$siteOption}"
|
||||
if [[ -z "$siteOld" ]]; then
|
||||
siteOld="${homeConst:-$homeOption}"
|
||||
fi
|
||||
if [[ -z "$siteOld" ]]; then
|
||||
appError "Could not detect siteOld (neither constants nor DB options present)."
|
||||
return 2
|
||||
fi
|
||||
local schemeOld hostOld rootOld
|
||||
schemeOld=$(printf '%s' "$siteOld" | awk -F:// '{print $1}')
|
||||
hostOld=$(printf '%s' "$siteOld" | awk -F[/:] '{print $4}')
|
||||
rootOld="$schemeOld://$hostOld"
|
||||
printInfo "$wordpressLabel Wordpress OLD | Site: $siteOld | Scheme: $schemeOld | Host: $hostOld | Root: $rootOld"
|
||||
|
||||
local sitePath homePath
|
||||
sitePath=$(wordpressExec "$domain" eval 'echo parse_url(get_option("siteurl"), PHP_URL_PATH)?:"";' || true)
|
||||
[[ "$sitePath" == "/" ]] && sitePath=""
|
||||
homePath=$(wordpressExec "$domain" eval 'echo parse_url(get_option("home"), PHP_URL_PATH)?:"";' || true)
|
||||
[[ "$homePath" == "/" ]] && homePath=""
|
||||
|
||||
local wpType="unknown"
|
||||
if [[ -z "$homePath" && -z "$sitePath" ]]; then wpType="single_root"
|
||||
elif [[ -n "$homePath" && -n "$sitePath" && "$homePath" == "$sitePath" ]]; then wpType="single_subdir"
|
||||
elif [[ -z "$homePath" && -n "$sitePath" ]]; then wpType="single_mixed"
|
||||
fi
|
||||
printInfo "$wordpressLabel Wordpress OLD | Type: $wpType"
|
||||
|
||||
local siteConstHas homeConstHas
|
||||
siteConstHas=$(wordpressExec "$domain" eval 'echo defined("WP_SITEURL")?1:0;')
|
||||
if [[ "$siteConstHas" == "1" ]]; then
|
||||
wordpressExec "$domain" config delete WP_SITEURL --type=constant || true
|
||||
fi
|
||||
homeConstHas=$(wordpressExec "$domain" eval 'echo defined("WP_HOME")?1:0;')
|
||||
if [[ "$homeConstHas" == "1" ]]; then
|
||||
wordpressExec "$domain" config delete WP_HOME --type=constant || true
|
||||
fi
|
||||
|
||||
printInfo "$wordpressLabel Update siteurl: $siteNew"
|
||||
wordpressExec "$domain" option update siteurl "$siteNew"
|
||||
|
||||
printInfo "$wordpressLabel Update home: $siteNew"
|
||||
wordpressExec "$domain" option update home "$siteNew"
|
||||
|
||||
printInfo "$wordpressLabel Replace in DB (1): $siteOld --> $siteNew"
|
||||
wordpressSearchReplace "$domain" "$siteOld" "$siteNew"
|
||||
if [[ -n "$homeOption" && "$homeOption" != "$siteOld" ]]; then
|
||||
printInfo "$wordpressLabel Replace in DB (2): $homeOption --> $siteNew"
|
||||
wordpressSearchReplace "$domain" "$homeOption" "$siteNew"
|
||||
fi
|
||||
if [[ -n "$siteOption" && "$siteOption" != "$siteOld" && "$siteOption" != "$homeOption" ]]; then
|
||||
printInfo "$wordpressLabel Replace in DB (3): $siteOption --> $siteNew"
|
||||
wordpressSearchReplace "$domain" "$siteOption" "$siteNew"
|
||||
fi
|
||||
if [[ -n "$hostOld" ]]; then
|
||||
printInfo "$wordpressLabel Replace in DB (4): //$hostOld --> $siteNew"
|
||||
wordpressSearchReplace "$domain" "//$hostOld" "$siteNew"
|
||||
fi
|
||||
if [[ "$wpType" == "single_mixed" ]]; then
|
||||
printInfo "$wordpressLabel Replace in DB (5): $rootOld --> $siteNew"
|
||||
wordpressSearchReplace "$domain" "$rootOld" "$siteNew"
|
||||
fi
|
||||
if [[ -n "$abspathOld" ]]; then
|
||||
printInfo "$wordpressLabel Replace in DB (6): $abspathOld --> $olsPodVhostsPath/$domain/www"
|
||||
wordpressSearchReplace "$domain" "$abspathOld" "$olsPodVhostsPath/$domain/www"
|
||||
fi
|
||||
|
||||
printInfo "$wordpressLabel Replace in DB (7): $hostOld --> $domain"
|
||||
wordpressSearchReplace "$domain" "$hostOld" "$domain"
|
||||
|
||||
printInfo "$wordpressLabel Delete options: upload_path/upload_url_path..."
|
||||
wordpressExec "$domain" option delete upload_path || true
|
||||
wordpressExec "$domain" option delete upload_url_path || true
|
||||
|
||||
printInfo "$wordpressLabel Clean cache..."
|
||||
wordpressExec "$domain" transient delete --all || true
|
||||
|
||||
printInfo "$wordpressLabel Redis clean..."
|
||||
redisDomainClean "$domain" || true
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
# Restart worker deployment and wait until ready.
|
||||
function workerRestart() {
|
||||
k3sRun rollout restart deployment/"$workerKube" || return 1
|
||||
k3sRun rollout status deployment/"$workerKube" --timeout=180s
|
||||
}
|
||||
|
||||
# Pause the worker agent via control file.
|
||||
function workerAgentStop() {
|
||||
printf '%s\n%s\n%s\n' "action=pause" "status=$taskStatusExecution" "start=$(date +%s)" > "$workerTasksPath/pause.task" || {
|
||||
appError "Failed to write pause task: $workerTasksPath/pause.task"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
# Remove pause flag for the worker agent.
|
||||
function workerAgentStart() {
|
||||
rm -f "$workerTasksPath/pause.task"
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
emojiSuccess=✅
|
||||
emojiWarning=💡
|
||||
emojiDanger=🔥
|
||||
emojiInfo=🔹
|
||||
|
||||
fontBlack=$'\033[0;30m'
|
||||
fontRed=$'\033[0;91m'
|
||||
fontGreen=$'\033[0;92m'
|
||||
fontYellow=$'\033[0;93m'
|
||||
fontBlue=$'\033[0;94m'
|
||||
fontMagenta=$'\033[0;95m'
|
||||
fontCyan=$'\033[0;96m'
|
||||
fontWhite=$'\033[0;97m'
|
||||
fontGray=$'\033[0;90m'
|
||||
|
||||
fontBold=$'\033[1m'
|
||||
fontDim=$'\033[2m'
|
||||
fontULine=$'\033[4m'
|
||||
fontReset=$'\033[0m'
|
||||
|
||||
labelDone="${fontGreen}done$fontReset"
|
||||
labelPass="${fontGreen}pass$fontReset"
|
||||
labelFail="${fontRed}fail$fontReset"
|
||||
labelOn="${fontGreen}on$fontReset"
|
||||
labelOff="${fontRed}off$fontReset"
|
||||
labelNull="${fontGray}null$fontReset"
|
||||
labelUnknown="${fontGray}unknown$fontReset"
|
||||
labelRunning="${fontGreen}running$fontReset"
|
||||
labelWarning="${fontYellow}warning$fontReset"
|
||||
labelDanger="${fontRed}danger$fontReset"
|
||||
|
||||
printWidth=80
|
||||
printFile=""
|
||||
|
||||
# Strip ANSI escape sequences from a string.
|
||||
# $1 (string): input string to strip.
|
||||
function strStripAnsi() {
|
||||
sed -r 's/\x1B\[[0-9;]*[A-Za-z]//g' <<<"$1"
|
||||
# LC_ALL=C sed -E $'s/\x1B\\[[0-?]*[ -/]*[@-~]//g' <<<"${1-}"
|
||||
}
|
||||
|
||||
# Write formatted output to stdout, the log file, and the print file (if configured).
|
||||
# $1 (text): text to write (printf %b formatting applied).
|
||||
function printWrite() {
|
||||
local plain logPath printPath
|
||||
|
||||
printf '%b' "$@"
|
||||
|
||||
# Log file
|
||||
[[ -n "${logFile:-}" ]] && {
|
||||
logPath=$(dirname -- "$printFile")
|
||||
[[ -d "$logPath" ]] || mkdir -p -- "$logPath" || return 0
|
||||
|
||||
printf '%s' "$@" | LC_ALL=C sed -E $'s/\x1B\\[[0-?]*[ -/]*[@-~]//g' >> "$logFile"
|
||||
}
|
||||
|
||||
# Print file
|
||||
[[ -n "${printFile:-}" ]] || return 0
|
||||
|
||||
printPath=$(dirname -- "$printFile")
|
||||
[[ -d "$printPath" ]] || mkdir -p -- "$printPath" || return 0
|
||||
|
||||
printf '%s' "$@" | LC_ALL=C sed -E $'s/\x1B\\[[0-?]*[ -/]*[@-~]//g' >> "$printFile"
|
||||
}
|
||||
|
||||
# Print a line padded to a fixed width with a filler character between prefix and suffix.
|
||||
# $1 (width): target line width in columns (0 or negative offsets from terminal width).
|
||||
# [$2] (filler): character used for padding (defaults to space).
|
||||
# [$3] (prefix): text printed before the fill area.
|
||||
# [$4] (suffix): text printed after the fill area.
|
||||
function printFill() {
|
||||
local term base plainBase baseLen fillLen
|
||||
local width="${1:-0}"
|
||||
local filler="${2:-}"
|
||||
local prefix="${3:-}"
|
||||
local suffix="${4:-}"
|
||||
shift 4
|
||||
|
||||
[[ -n "$filler" ]] || filler=" "
|
||||
|
||||
term=$(tput cols 2>/dev/null || echo 80)
|
||||
(( width <= 0 )) && width=$((term + width))
|
||||
(( width <= 0 )) && width=0
|
||||
|
||||
base="$prefix$suffix"
|
||||
plainBase=$(strStripAnsi "$base")
|
||||
baseLen=${#plainBase}
|
||||
if (( baseLen >= width )); then
|
||||
# printf '%s\n' "$base"
|
||||
printWrite "$base"$'\n'
|
||||
return 0
|
||||
fi
|
||||
|
||||
fillLen=$((width - baseLen))
|
||||
printf -v fill '%*s' "$fillLen" ''
|
||||
fill="${fill// /$filler}"
|
||||
printWrite "$prefix$fill$suffix$*"$'\n'
|
||||
}
|
||||
|
||||
# Print a dot-filled line with a label on the left and a value on the right.
|
||||
# [$1] (width): line width (defaults to 60).
|
||||
# [$2] (label): left-side label text.
|
||||
# [$3] (value): right-side value text.
|
||||
function printDot() {
|
||||
printFill "${1:-$printWidth}" "." "${2:-} $fontGray" "$fontReset ${3:-}" "${@:4}"
|
||||
}
|
||||
|
||||
function printDotText() {
|
||||
printDot "$printWidth" "$@"
|
||||
}
|
||||
|
||||
|
||||
# Print a dot-filled line with a label on the left and no right-side value (fixed layout).
|
||||
# [$1] (width): line width (defaults to 60).
|
||||
# [$2] (label): left-side label text.
|
||||
function printDotFix() {
|
||||
printFill "${1:-$printWidth}" "." "${2:-} $fontGray" "$fontReset " "${@:3}"
|
||||
}
|
||||
|
||||
# Print a horizontal rule or a blank line.
|
||||
# [$1] (filler): character to repeat (empty prints a blank line).
|
||||
# [$2] (width): line width (defaults to terminal width).
|
||||
# [$3] (prefix): text before the fill.
|
||||
# [$4] (suffix): text after the fill.
|
||||
function printRow() {
|
||||
local filler="${1:-}"
|
||||
local width="${2:-0}"
|
||||
local prefix="${3:-}"
|
||||
local suffix="${4:-}"
|
||||
|
||||
if [[ "$filler" == '' && "$width" = 0 ]]; then
|
||||
printWrite $'\n'
|
||||
else
|
||||
printFill "$width" "$filler" "$prefix" "$suffix"
|
||||
fi
|
||||
}
|
||||
|
||||
# Print a plain text line followed by a newline.
|
||||
# $1 (text): text to print.
|
||||
function printText() {
|
||||
printWrite "$@"$'\n'
|
||||
}
|
||||
|
||||
# Print a success message in green with a success emoji.
|
||||
# $1 (message): message text.
|
||||
function printSuccess() {
|
||||
printWrite "$emojiSuccess$fontGreen$@$fontReset"$'\n'
|
||||
}
|
||||
|
||||
# Print a danger/error message in red with a danger emoji.
|
||||
# $1 (message): message text.
|
||||
function printDanger() {
|
||||
printWrite "$emojiDanger$fontRed$@$fontReset"$'\n'
|
||||
}
|
||||
|
||||
# Print a warning message in yellow with a warning emoji.
|
||||
# $1 (message): message text.
|
||||
function printWarning() {
|
||||
printWrite "$emojiWarning$fontYellow$@$fontReset"$'\n'
|
||||
}
|
||||
|
||||
# Print an informational message in blue with an info emoji.
|
||||
# $1 (message): message text.
|
||||
function printInfo() {
|
||||
printWrite "$emojiInfo$fontBlue$@$fontReset"$'\n'
|
||||
}
|
||||
|
||||
# Print a section title preceded by a blank line, optionally with a custom color.
|
||||
# $1 (title): title text.
|
||||
# [$2] (color): ANSI color code (defaults to fontBlue).
|
||||
function printTitle() {
|
||||
local fontColor=${2:-$fontBlue}
|
||||
|
||||
printWrite $'\n'"$fontColor$1$fontReset"$'\n'
|
||||
}
|
||||
|
||||
# Print a decorative section header box with a title.
|
||||
# $1 (title): section title text.
|
||||
# [$2] (color): ANSI color code (defaults to fontBlue).
|
||||
function printSection() {
|
||||
local fontColor=${2:-$fontBlue}
|
||||
printRow
|
||||
printFill "$printWidth" '─' " ${fontColor}┌─[ $1 ]" "┐ $fontReset"
|
||||
}
|
||||
|
||||
# Print a start marker with the current app date and time.
|
||||
function printStart() {
|
||||
printDotText "Start" "$appDate ${appTime//-/:}"
|
||||
printRow
|
||||
}
|
||||
|
||||
# Print a finish marker with the current timestamp and total elapsed execution time.
|
||||
function printFinish() {
|
||||
local label
|
||||
label=$(timeDiff "$appDate ${appTime//-/:}" "$(date +%Y-%m-%d) $(date +%H:%M:%S)")
|
||||
|
||||
printRow
|
||||
printDotText "Finish" "$(date +%Y-%m-%d) $(date +%H:%M:%S)"
|
||||
printDotText "Execution time" "${label}s"
|
||||
}
|
||||
|
||||
# Print the application header banner to stdout and write a separator to the log file.
|
||||
function printHeader() {
|
||||
local label tmpPath fillLen fill
|
||||
|
||||
label="$appName $appVersion | $appDate $appTime"
|
||||
printf "\e[48;5;74m %-*s \e[0m\n" $(( printWidth - 2 )) "$label"
|
||||
|
||||
[[ -n "${logFile:-}" ]] || return 0
|
||||
|
||||
tmpPath=$(dirname -- "$logFile")
|
||||
[[ -d "$tmpPath" ]] || mkdir -p -- "$tmpPath" || return 0
|
||||
|
||||
fillLen=$(($printWidth - 5 - ${#label}))
|
||||
printf -v fill '%*s' "$fillLen" ''
|
||||
fill="${fill// /═}"
|
||||
printf "[ %s ]\n" "$label $fill" >> "$logFile"
|
||||
}
|
||||
|
||||
# Print a goodbye footer with total elapsed execution time to stdout and the log file.
|
||||
function printFooter() {
|
||||
local label
|
||||
label=$(timeDiff "$appDate ${appTime//-/:}" "$(date +%Y-%m-%d) $(date +%H:%M:%S)")
|
||||
|
||||
printf '\n%b\n' "Bye bye | Time: ${label}s"
|
||||
printf '\n%b\n' "Bye bye | Time: ${label}s" >> "$logFile"
|
||||
}
|
||||
Reference in New Issue
Block a user