#!/usr/bin/env bash ### Copyright 1999-2025. WebPros International GmbH. All rights reserved. # --- utility functions --- p_echo() { echo "$@" >&2 } write() { local filename="$1" local directory=`dirname "$filename"` mkdirectory "$directory" if [ "x$?" != "x0" ] ; then ERROR=$? p_echo "Can not create directory $directory" return $ERROR fi # Check target file can be replaced if [ -f "$filename" ] ; then if [ ! -w "$filename" -o ! -r "$filename" ] ; then p_echo "Can not read/write to $filename" return 100 fi fi # Put new content into temporary file local tmpfile tmpfile=`mktemp "$filename".XXXXXX` if [ "x$?" != "x0" -o ! -w "$tmpfile" ] ; then p_echo "Can not create temporary file $tmpfile" return 101 fi # Correct owner & permissions chown "$FILE_OWNERGROUP" "$tmpfile" && chmod "$FILE_PERMS" "$tmpfile" cat > "$tmpfile" if [ "x$?" != "x0" ] ; then p_echo "Error writing to $tmpfile" rm -f "$tmpfile" return 101 fi # Check that new file is not empty if [ ! -s "$tmpfile" ] ; then p_echo "No or empty input supplied on utility's standard input stream!" rm -f "$tmpfile" return 101 fi # Commit changes to target file (disable interactivity in mv) mv -f "$tmpfile" "$filename" if [ "x$?" != "x0" ] ; then ERROR=$? rm -f "$tmpfile" return $ERROR fi return $? } mklink() { if ! echo "$1" | grep -q ':' 2>/dev/null; then p_echo "Invalid format for mklink: wait source:destinstation, got $1" return 102 fi local filename="${1%%:*}" local destination="${1#*:}" local directory=`dirname "$filename"` local destination_directory=`dirname "$destination"` if ! [ -d "$directory" -a -f "$filename" ]; then p_echo "Path $filename doesn't exist" return 100 fi if [ ! -d "$destination_directory" ]; then p_echo "Destination directory '$destination_directory' not exist" return 100 fi if [ -f "$destination" -a ! -L "$destination" ]; then p_echo "Refusing to create symlink '$destination': file with the same name already exists" return 101 fi local fname=`basename "$filename"` local lnkname="last_${fname#*_}" # Create convenience symlink to last written config pushd "$directory" if [ ! -f "$lnkname" -o -L "$lnkname" ] ; then ln -snfT "$fname" "$lnkname" else p_echo "Refusing to create symlink '$directory/$lnkname': file with the same name already exists" ERROR=101 fi popd # Create symlink for argument ln -snfT "$filename" "$destination" local tmp_err="$?" [ "$tmp_err" = "0" ] || ERROR="$tmp_err" return $ERROR } mkdirectory() { local path="$1" if [ -d "$path" ] ; then return 0 fi if [ -e "$path" ] ; then p_echo "File $path already exists" return 100 fi mkdir "$path" if [ "x$?" != "x0" ] ; then ERROR=$? return $ERROR fi chmod "$DIR_PERMS" "$path" if [ "x$?" != "x0" ] ; then ERROR=$? return $ERROR fi chown "$DIR_OWNERGROUP" "$path" if [ "x$?" != "x0" ] ; then ERROR=$? return $ERROR fi return $? } check() { local id file while read data; do if [ "x$data" != "x" ]; then id=${data%%:*}; file=${data#*:}; if [ ! -f $file ]; then echo "$id:File '$file' not found"; fi fi done return 0 } backup_file() { local filename="$1" [ ! -f "$filename" ] && return 0 cp -f "$filename" "$filename.bak" chown "$FILE_OWNERGROUP" "$filename.bak" && chmod "$FILE_PERMS" "$filename.bak" return $? } restore_file() { local filename="$1" [ ! -f "$filename.bak" ] && return 0 cp -f "$filename.bak" "$filename" chown "$FILE_OWNERGROUP" "$filename" && chmod "$FILE_PERMS" "$filename" return $? } #!/usr/bin/env bash ### Copyright 1999-2025. WebPros International GmbH. All rights reserved. usage() { cat << EOH Usage: $0 [options] Helper utility to manage NGINX configuration files OPTIONS: -t - Test and fix NGINX configuration if possible. -T - Just test NGINX configuration. -d dir - Create directory. -w file - Overwrite or create specified file with content from stdin. -b file - Create backup copy of specified file. -r file - Restore backup copy of specified file if present. -l file:destination - Switch or create symlink to the specified file and switch or create 'last_*' symlink -c - Read configuration files list from stdin and check their their presence. Each line should be like ':'. EOH } # --- nginx-specific --- set_params() { DIR_OWNERGROUP="nginx":"psacln" DIR_PERMS=770 FILE_OWNERGROUP="root":"nginx" FILE_PERMS=600 NGINX_BIN="/usr/sbin/nginx" NGINX_INCLUDE_D="/etc/nginx/conf.d" NGINX_RC_CONFIG="/etc/default/nginx" PRODUCT_ROOT_D="/opt/psa" } get_cur_value() { local msg="$1" local param="$2" echo "$msg" | sed -ne 's/^.*'$param':[[:space:]]*\([[:digit:]]\+\).*$/\1/p' | tail -n 1 } update_conf_value() { local param="$1" local value="$2" local config="$NGINX_INCLUDE_D/aa500_psa_tweaks.conf" echo "Updating config value: $param = $value" if grep -q "^\s*$param" "$config" >/dev/null 2>&1 ; then sed -e 's/^\(\s*'$param'\s*\)[^;#]*\(;\s*\(#.*\)\?\)$/\1'$value'\2/g' "$config" > "$config.tmp" && mv -f "$config.tmp" "$config" || rm -f "$config.tmp" else echo "$param $value;" >> "$config" fi chown "$FILE_OWNERGROUP" "$config" && chmod "$FILE_PERMS" "$config" } get_approx_server_names_num() { local bootstrap="/etc/nginx/conf.d/zz010_psa_nginx.conf" local num add num=0 for config in `sed -ne 's/^\s*include\s*\([^;]*\);/\1/p' "$bootstrap" 2>/dev/null`; do add=`awk '/^[[:space:]]*server_name[[:space:]]*/ { ++count } END { print count }' "$config" 2>/dev/null` [ -z "$add" ] || num=$(( num + add )) done echo "$num" } check_conf() { local NGINX_ULIMIT= # NGINX also tries to open all log files on configuration test. Set ulimits, if any. if [ -f "/lib/systemd/system/nginx.service.d/limit_nofile.conf" ]; then NGINX_ULIMIT=`sed -n 's|^[[:space:]]*LimitNOFILE[[:space:]]*=[[:space:]]*\([[:digit:]]\+\).*$|\1|p' "/lib/systemd/system/nginx.service.d/limit_nofile.conf" 2>/dev/null | tail -n1` [ -z "${NGINX_ULIMIT}" ] || NGINX_ULIMIT="-n ${NGINX_ULIMIT}" elif [ -f "$NGINX_RC_CONFIG" ]; then . "$NGINX_RC_CONFIG" fi [ -z "$NGINX_ULIMIT" ] || ulimit $NGINX_ULIMIT $NGINX_BIN -qt } check_and_fix_conf() { local msg local server_names_hash_max_size server_names_hash_bucket_size local server_names_num max_server_names_hash_max_size # This value is actually variable (most common values being 32 and 64), # but it doesn't really matter much for following calculations. # Just assume 64 as the most common biggest cache line size. local min_bucket_size=64 msg=`check_conf 2>&1`; ERROR=$? while [ "$ERROR" -ne 0 ] || \ echo "$msg" | grep -q 'could not build the server_names_hash' 2>/dev/null || \ echo "$msg" | grep -q 'could not build optimal server_names_hash' 2>/dev/null do # Tweaking NGINX hash parameters (*_hash_max_size and *_hash_bucket_size) is quite tricky. # Internally NGINX attempts to build a minimal hashing such that each bucket size is *_hash_bucket_size # (which should fit into as few cache lines as possible, ideally 1), and total number of buckets in hash # table is the minimal possible value that does not exceed *_hash_max_size. Therefore when we are asked # to increase either of parameters we should first try to increase *_hash_max_size. Increasing it way # past amount of items in hashing (e.g. server names) makes hash table too sparse which increases memory # consumption. If the error persists this means that there are simply too many collisions - therefore we # should then increase *_hash_bucket_size value. # If NGINX configuration test takes too long this might also mean that we need to increase # *_hash_bucket_size instead of *_hash_max_size. The maximum time should grow as O( ^ 2) # assuming that *_hash_max_size is the same order of magnitude as number of items in a hashing. This # code doesn't implement such logic as it is too error prone and hard to test. Therefore occasionally # user might still need to tweak these parameters manually. # Note that there are at least 3 sets of hashes employed by NGINX: variables, types and server_names. # As nothing but the latter one is used by Plesk, we handle only server_names parameters here. server_names_hash_max_size=` get_cur_value "$msg" server_names_hash_max_size` server_names_hash_bucket_size=`get_cur_value "$msg" server_names_hash_bucket_size` if [ -z "$server_names_hash_max_size" -a -z "$server_names_hash_bucket_size" ]; then # some error not related to server_names_hash_max_size or server_names_hash_bucket_size occurs # e.g. syntax error in configuration break fi if [ -n "$server_names_hash_max_size" -a -z "$server_names_num" ]; then server_names_num=`get_approx_server_names_num` max_server_names_hash_max_size=$(( server_names_num * 6 )) fi # The order of checks is important! if [ -n "$server_names_hash_max_size" ] && [ "$server_names_hash_max_size" -lt "$max_server_names_hash_max_size" ]; then # At most 4 (= 6/2 + 1) attempts to increase server_names_hash_max_size will be made if [ "$server_names_hash_max_size" -lt "$server_names_num" ]; then server_names_hash_max_size=$server_names_num else server_names_hash_max_size=$(( server_names_hash_max_size + server_names_num * 2 )) [ "$server_names_hash_max_size" -gt "$max_server_names_hash_max_size" ] && server_names_hash_max_size="$max_server_names_hash_max_size" fi update_conf_value "server_names_hash_max_size" "$server_names_hash_max_size" elif [ -n "$server_names_hash_bucket_size" ]; then # $(( server_names_hash_bucket_size + 1 )) would produce same results due to internal NGINX handling, # but let's put a clear and accurate value into config. server_names_hash_bucket_size=$(( (server_names_hash_bucket_size + 1 + (min_bucket_size - 1)) & ~(min_bucket_size - 1) )) update_conf_value "server_names_hash_bucket_size" "$server_names_hash_bucket_size" fi msg=`check_conf 2>&1`; ERROR=$? done [ -n "$msg" ] && p_echo "$msg" return $ERROR } # --- script --- if [ $# -eq 0 ] ; then usage exit 0 fi getopts "Ttd:w:l:cb:r:" OPTION set_params ERROR=0 case $OPTION in T) check_conf ERROR=$? ;; t) check_and_fix_conf ERROR=$? ;; d) mkdirectory "$OPTARG" ERROR=$? ;; w) write "$OPTARG" ERROR=$? ;; l) mklink "$OPTARG" ERROR=$? ;; c) check ERROR=$? ;; b) backup_file "$OPTARG" ERROR=$? ;; r) restore_file "$OPTARG" ERROR=$? ;; *) usage ERROR=1 ;; esac exit $ERROR