#!/usr/bin/env bash

# --- utility functions ---

write() {

    local filename="$1"

    local directory=`dirname "$filename"`
    mkdirectory "$directory"
    if [ "x$?" != "x0" ] ; then
        ERROR=$?
        echo "Can not create directory $directory" >&2
        return $ERROR
    fi

    # Check target file can be replaced
    if [ -f "$filename" ] ; then
        if [ ! -w "$filename" -o ! -r "$filename" ] ; then
            echo "Can not read/write to $filename" >&2
            return 100
        fi
    fi

    # Put new content into temporary file
    local tmpfile=`mktemp "$filename".XXXXXX`
    if [ "x$?" != "x0" -o ! -w "$tmpfile" ] ; then
        echo "Can not create temporary file $tmpfile" >&2
        return 101
    fi

    cat > "$tmpfile"
    if [ "x$?" != "x0" ] ; then
        echo "Error writing to $tmpfile" >&2
        rm -f "$tmpfile"
        return 101
    fi

    # Check that new file is not empty
    if [ ! -s "$tmpfile" ] ; then
        echo "No or empty input supplied on utility's standard input stream!" >&2
        rm -f "$tmpfile"
        return 101
    fi

    # Remove existing target file
    if [ -f "$filename" ] ; then
        rm -f "$filename"
        if [ "x$?" != "x0" ] ; then
            echo "Can not remove existing file $filename" >&2
            rm -f "$tmpfile"
            return 100
        fi
    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

    # Correct owner & permissions
    chown "$FILE_OWNERGROUP" "$filename" && chmod "$FILE_PERMS" "$filename"

    return $?
}

mklink() {
    local filename="$1"
    local directory=`dirname "$filename"`

    if ! [ -d "$directory" -a -f "$filename" ]; then
        echo "Path $filename doesn't exist"
        return 100
    fi

    # Create convenience symlink to last written config
    local fname=`basename "$filename"`
    local lnkname="last_${fname#*_}"
    if ! echo "${fname%%_*}" | grep -q '[^0-9.]' 2>/dev/null ; then
        pushd "$directory"
        if [ ! -f "$lnkname" -o -L "$lnkname" ] ; then
            rm -f "$lnkname" && ln -s "$fname" "$lnkname"
            ERROR=$?
        else
            echo "Refusing to create symlink '$directory/$lnkname': file with the same name already exists"
            ERROR=101
        fi
        popd
    else
        echo "Refusing to create symlink: unexpected file name format"
        ERROR=101
    fi

    return $ERROR
}

mkdirectory() {

    local path="$1"

    if [ -d "$path" ] ; then
        return 0
    fi

    if [ -e "$path" ] ; then
        echo "File $path already exists" >&2
        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
}

#!/usr/bin/env bash

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.
 -l file - Switch or create 'last_*' symlink to the specified file.
 -c      - Read configuration files list from stdin and check their
           their presence. Each line should be like '<id>:<filepath>'.

EOH
}

# --- nginx-specific ---

set_params()
{
	DIR_OWNERGROUP="nginx":"psacln"
	DIR_PERMS=770
	FILE_OWNERGROUP="root":"nginx"
	FILE_PERMS=640
	NGINX_BIN="/usr/sbin/nginx"
	NGINX_INCLUDE_D="/etc/nginx/conf.d"
	PRODUCT_ROOT_D="/usr/local/psa"
}

get_cur_value()
{
	local msg="$1"
	local param="$2"
	echo "$msg" | sed -ne 's/^.*'$param':[[:space:]]*\([[:digit:]]\+\).*$/\1/p'
}

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="$PRODUCT_ROOT_D/admin/conf/nginx_vhosts_bootstrap.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`
		num=$(( num + add ))
	done

	echo "$num"
}

check_conf()
{
	$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 ]; do
		if echo "$msg" | grep -q 'could not build the server_names_hash' 2>/dev/null ; then
			# 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(<hashing items> ^ 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 [ -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
		else
			break
		fi

		msg=`check_conf 2>&1`; ERROR=$?
	done

	[ -n "$msg" ] && echo "$msg" >&2
	return $ERROR
}

# --- script ---

if [ $# -eq 0 ] ; then
	usage
	exit 0
fi

getopts "Ttd:w:l:c" 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=$?
		;;
	*)
		usage
		ERROR=1
		;;
esac

exit $ERROR

