#!/usr/bin/env bash
#
# Function description:
# Use this script to initialize system after full refresh installation
# Do NOT modify anything expect for "user defined variables" unless you know what it means and what are you doing.

# Try most best to refer to more general and minimal principle, UNIX philosophy
#
# Usage:
# bash centos6-init.sh
#
# Birth Time:
# 2016-05-17 10:19:33.005064327 +0800
#
# Author:
# Open Source Software written by 'Guodong Ding <dgdenterprise@gmail.com>'
# Blog: http://dgd2010.blog.51cto.com/
# Github: https://github.com/DingGuodong
#

# debug option
#_XTRACE_FUNCTIONS=$(set +o | grep xtrace)
#set -o xtrace

# define user friendly messages
header="
Use this script to initialize system after full refresh installation
"

# user defined variables
user_defined_hostname=""
user_defined_username=""
user_defined_user_can_run_sudo=true # true or false
user_defined_log_absolute_path=""
# end user defined variables

# pretreatment
# end pretreatment

# Public header
# =============================================================================================================================
# resolve links - $0 may be a symbolic link
# learn from apache-tomcat-6.x.xx/bin/catalina.sh
PRG="$0"

while [ -h "$PRG" ]; do
  ls=$(ls -ld "$PRG")
  link=$(expr "$ls" : '.*-> \(.*\)$')
  if expr "$link" : '/.*' >/dev/null; then
    PRG="$link"
  else
    PRG=$(dirname "$PRG")/"$link"
  fi
done

# Get standard environment variables
PRGDIR=$(dirname "$PRG")

# echo color function, smarter, learn from lnmp.org lnmp install.sh
function echo_r() {
  # Color red: Error, Failed
  [ $# -ne 1 ] && return 1
  echo -e "\033[31m$1\033[0m"
}

function echo_g() {
  # Color green: Success
  [ $# -ne 1 ] && return 1
  echo -e "\033[32m$1\033[0m"
}

function echo_y() {
  # Color yellow: Warning
  [ $# -ne 1 ] && return 1
  echo -e "\033[33m$1\033[0m"
}

function echo_b() {
  # Color blue: Debug Level 1
  [ $# -ne 1 ] && return 1
  echo -e "\033[34m$1\033[0m"
}

function echo_p() {
  # Color purple: Debug Level 2
  [ $# -ne 1 ] && return 1
  echo -e "\033[35m$1\033[0m"
}

function echo_c() {
  # Color cyan: friendly prompt, Level 1
  [ $# -ne 1 ] && return 1
  echo -e "\033[36m$1\033[0m"
}

# end echo color function, smarter

#WORKDIR="`realpath ${WORKDIR}`"
WORKDIR=$(readlink -f "${PRGDIR}")

# end public header
# =============================================================================================================================

USER="$(id -un)"
LOGNAME="$USER"
if [ $UID -ne 0 ]; then
  echo_y "WARNING: Running as a non-root user, \"$LOGNAME\". Functionality may be unavailable. Only root can use some commands or options."
  echo_y "Retry to using \"sudo bash $0 $*\"."
fi

command_exists() {
  # which "$@" >/dev/null 2>&1
  command -v "$@" >/dev/null 2>&1
}

check_command_can_be_execute() {
  [ $# -ne 1 ] && return 1
  command_exists "$1"
}

function check_user_defined_variables() {
  test -z ${user_defined_hostname}
  test -z ${user_defined_username}
  test -z ${user_defined_user_can_run_sudo}
  test -z ${user_defined_log_absolute_path}

}

function backup_single_file() {
  set -o errexit
  if [ "$#" -ne 1 ]; then
    return 1
  fi
  backup_filename_origin="$1"
  operation_date_time=$(date +"%Y%m%d%H%M%S")
  backup_filename_prefix=".backup_"
  backup_filename_suffix="_origin_$operation_date_time~"
  backup_filename_target="$backup_filename_prefix$backup_filename_origin$backup_filename_suffix"
  test -f "${backup_filename_origin}" && cp "${backup_filename_origin}" "${backup_filename_target}"
  set +o errexit

}

# Function description: backup files without directory support
# Note: accept $@ parameters, such as '$0 file1 file2 file3'
# Usage: backup_files $@ || rollback_files
backup_files() {
  set -o errexit
  if [ "$#" -eq 0 ]; then
    return 1
  fi
  file_list=$*
  operation_date_time="_$(date +"%Y%m%d%H%M%S")"
  log_filename=".log_$0_$$_$RANDOM"
  if test -z ${user_defined_log_absolute_path}; then
    log_filename_full_path=/tmp/${log_filename}
  else
    log_filename_full_path=${user_defined_log_absolute_path}
  fi
  touch ${log_filename_full_path}
  old_IFS=$IFS
  IFS=" "
  for file in ${file_list}; do
    # is there a bin named 'realpath' ?
    real_file=$(readlink -f "${file}")
    [ -f "${real_file}" ] && cp "${real_file}" "${file}${operation_date_time}~"
    [ -f ${log_filename_full_path} ] && echo "\mv -f $file$operation_date_time~ $file" >>${log_filename_full_path}
  done
  IFS="$old_IFS"
  set +o errexit
  return 0
}

# Function description: rollback files
rollback_files() {
  # shellcheck source=/tmp/some.sh
  [ -f ${log_filename_full_path} ] && . ${log_filename_full_path}
  \rm -f ${log_filename_full_path}
  exit 2
}

check_network_connectivity() {
  echo_b "checking network connectivity ... "
  network_address_to_check=8.8.4.4
  stable_network_address_to_check=114.114.114.114
  ping_count=2
  ping -c ${ping_count} ${network_address_to_check} >/dev/null
  retval=$?
  if [ ${retval} -ne 0 ]; then
    if ping -c ${ping_count} ${stable_network_address_to_check} >/dev/null; then
      echo_g "Network to $stable_network_address_to_check succeed! "
      echo_y "Note: network to $network_address_to_check failed once! maybe just some packages loss."
    elif ! ip route | grep default >/dev/null; then
      echo_r "Network is unreachable, gateway is not set."
      exit 1
    elif ! ping -c2 "$(ip route | awk '/default/ {print $3}')" >/dev/null; then
      echo_r "Network is unreachable, gateway is unreachable."
      exit 1
    else
      echo_r "Network is blocked! "
      exit 1
    fi
  elif [ ${retval} -eq 0 ]; then
    echo_g "Check network connectivity passed! "
  fi
}

check_name_resolve() {
  echo_b "checking DNS name resolve ... "
  target_name_to_resolve="github.com"
  stable_target_name_to_resolve="www.aliyun.com"
  ping_count=1
  if ! ping -c${ping_count} ${target_name_to_resolve} >/dev/null; then
    echo_y "Name lookup failed for $target_name_to_resolve with $ping_count times "
    if ping -c${ping_count} ${stable_target_name_to_resolve} >/dev/null; then
      echo_g "Name lookup success for $stable_target_name_to_resolve with $ping_count times "
    fi
    eval_md5sum_of_nameserver_config="$(md5sum /etc/resolv.conf | awk '{ print $1 }')"
    if test "${eval_md5sum_of_nameserver_config}" = "674ea91675cdfac353bffbf49dc593c3"; then
      echo_y "Nameserver config file is validated, but name lookup failed for $target_name_to_resolve with $ping_count times"
      return 0
    fi
    [ -f /etc/resolv.conf ] && cp /etc/resolv.conf "/etc/resolv.conf_$(date +%Y%m%d%H%M%S)~"
    cat >/etc/resolv.conf <<eof
nameserver 114.114.114.114
nameserver 8.8.4.4
eof
    check_name_resolve
  else
    echo_g "Check DNS name resolve passed! "
    return 0
  fi
}

function set_hosts_file() {
  hosts_url="https://raw.githubusercontent.com/racaljk/hosts/master/hosts"
  http_code=$(curl -o /dev/null -m 10 --connect-timeout 10 -s -w "%{http_code}" ${hosts_url})
  if test "${http_code}" -ne 200; then
    hosts_url="https://coding.net/u/scaffrey/p/hosts/git/raw/master/hosts"
  fi
  backup_single_file /etc/hosts
  bash -c 'wget ${hosts_url} -qO /tmp/hosts && mv -f /tmp/hosts /etc/hosts'
  # TODO(Guodong Ding) dos2unix
  #    if cat -A /etc/hosts | grep '\^M\\\$' >/dev/null || file /etc/hosts | grep "with CRLF line terminators" >/dev/null ; then
  #        which dos2unix >/dev/null 2>&1 || yum -q -y install dos2unix || apt-get -qq -y install dos2unix
  #        dos2unix /etc/hosts >/dev/null
  #    fi

  # check if able to resolve host `hostname -f`, if not, sudo will throw a exception 'sudo: unable to resolve host xxx'
  echo "127.0.0.1 $(hostname) $(hostname -f)" >>/etc/hosts
  echo "$(ip addr show scope global "$(ip route | awk '/^default/ {print $NF}')" | awk -F '[ /]+' '/global/ {print $3}') $(hostname) $(hostname -f)" >>/etc/hosts

  SELINUX_STATE=$(cat "/selinux/enforce")
  [ -n "$SELINUX_STATE" ] && [ -x /sbin/restorecon ] && /sbin/restorecon /etc/hosts

}

function set_hostname_fqdn_format() {
  current_hostname_fqdn="$(hostname -A)"
  dot_appear_times_to_match_fqdn_rule=$(echo "$current_hostname_fqdn" | grep -o '\.' | wc -l)
  if test "${dot_appear_times_to_match_fqdn_rule}" -gt 1; then
    echo_g "current hostname $current_hostname_fqdn is a fqdn name, check passed! "
  else
    if test ! -z ${user_defined_hostname}; then
      new_hostname_to_set="$user_defined_hostname"
    else
      read -rp 'Input hostname you want, then press Enter ' user_input_hostname
      new_hostname_to_set="$user_input_hostname"
    fi
    test -f /etc/hostname && echo "$new_hostname_to_set" >/etc/hostname
    (test -f /etc/hostname && hostname -b -F /etc/hostname) || hostname "$new_hostname_to_set"
    test -f /etc/sysconfig/network && sed -i "s/^HOSTNAME=.*.$/HOSTNAME=$new_hostname_to_set/g" /etc/sysconfig/network
  fi
  ipaddress_global_routing="$(ip addr show scope global "$(ip route | awk '/^default/ {print $NF}')" | awk -F '[ /]+' '/global/ {print $3}')"
  grep -v -E "($ipaddress_global_routing|$new_hostname_to_set)" /etc/hosts >/dev/null 2>&1 && echo "$ipaddress_global_routing $new_hostname_to_set" >>/etc/hosts
}

function yum_install_base_packages() {
  yum -y install vim wget curl perl unzip man man-pages man-pages-overrides bind-utils net-tools >/dev/null 2>&1
}

function yum_repository_config() {
  yum -y install http://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm >/dev/null 2>&1

}

function yum_install_extra_packages() {
  # Programmable completion for Bash
  yum info bash-completion >/dev/null 2>&1 && yum -y install bash-completion >/dev/null 2>&1
  # Ask the user to install command line programs automatically
  yum info PackageKit-command-not-found >/dev/null 2>&1 && yum -y install PackageKit-command-not-found >/dev/null 2>&1
  # Bash tab completion for argparse
  yum info python-argcomplete >/dev/null 2>&1 && yum -y install python-argcomplete >/dev/null 2>&1
  yum -y update

}

function set_limits() {
  backup_single_file /etc/security/limits.conf
  cat >>/etc/security/limits.conf <<eof
# refer to the free inodes number, by "df -i" commands
* soft nofile 200000
* hard nofile 200000
* soft nproc 16384
* hard nproc 16384
eof
  if [[ -f /etc/security/limits.d/90-nproc.conf ]]; then
    mv /etc/security/limits.d/90-nproc.conf /etc/security/limits.d/90-nproc.conf~
  fi

}

function customized_commands() {
  # customized commands, alternatively, echo into .bashrc file as a function or alias
  cat >/usr/local/bin/delsc <<'eof'
#!/bin/bash
# delete all spaces and comments of specialized file
[[ "$1" == "" ]] && echo "usage: delete all spaces and comments of specialized file, using with \"$0 filename\"" && exit 1
if cat -A "$1" | grep '\^M\$' >/dev/null || file "$1" | grep "with CRLF line terminators" >/dev/null ; then
    command -v dos2unix >/dev/null 2>&1 || yum -q -y install dos2unix || apt-get -qq -y install dos2unix
    dos2unix "$1" >/dev/null
fi
if test -f "$1" && file "$1" | grep "XML" >/dev/null; then
    command -v tidy >/dev/null 2>&1 || yum -q -y install tidy || apt-get -qq -y install tidy
    tidy -quiet -asxml -xml -indent -wrap 1024 --hide-comments 1 "$1"
elif test -f "$1"; then
    grep -v \# "$1" | grep -v ^\; |grep -v ^$ | grep -v "^\ *$"
fi
# Others:
# sed -e '/^#/d;/^$/d'
# Refer: https://github.com/mysfitt/nocomment/blob/master/nocomment.sh
# grep -Ev '^\s*#|^//|^\s\*|^/\*|^\*/'  | grep -Ev '^$|^\s+$'
eof
  chmod +x /usr/local/bin/delsc
  SELINUX_STATE=$(cat "/selinux/enforce")
  [ -n "$SELINUX_STATE" ] && [ -x /sbin/restorecon ] && /sbin/restorecon -r /usr/local/bin

}

function inject_ssh_key_for_root() {
  ## Allow us to SSH passwordless to localhost
  # ssh-keygen -f ~/.ssh/id_rsa -N ""
  # cp ~/.ssh/{id_rsa.pub,authorized_keys}
  ## Creation of an SSH agent for testing forwarding
  # eval $(ssh-agent)
  # ssh-add
  test ! -d /root/.ssh && ssh-keygen -N "" -f /root/.ssh/id_rsa
  test ! -f /root/.ssh/authorized_keys && cp /root/.ssh/id_rsa.pub /root/.ssh/authorized_keys
  cat >>/root/.ssh/authorized_keys <<eof
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCawuOgQup3Qc1OILytyH+u3S9te85ctEKTvzPtRjHfnEEOjpRS6v6/PsuDHplHO1PAm8cKbEZmqR9tg4mWSweosBYW7blUUB4yWfBu6cHAnJOZ7ADNWHHJHAYi8QFZd4SLAAKbf9J12Xrkw2qZkdUyTBVbm+Y8Ay9bHqGX7KKLhjt0FIqQHRizcvncBFHXbCTJWsAduj2i7GQ5vJ507+MgFl2ZTKD2BGX5m0Jq9z3NTJD7fEb2J6RxC9PypYjayXyQBhgACxaBrPXRdYVXmy3f3zRQ4/OmJvkgoSodB7fYL8tcUZWSoXFa33vdPlVlBYx91uuA6onvOXDnryo3frN1
ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAumQ2srRwd9slaeYTdr/dGd0H4NzJ3uQdBQABTe/nhJsUFWVG3titj7JiOYjCb54dmpHoi4rAYIElwrolQttZSCDKTVjamnzXfbV8HvJapLLLJTdKraSXhiUkdS4D004uleMpaqhmgNxCLu7onesCCWQzsNw9Hgpx5Hicpko6Xh0=
eof
  SELINUX_STATE=$(cat "/selinux/enforce")
  ([ -n "$SELINUX_STATE" ] && [ -x /sbin/restorecon ] && /sbin/restorecon -r /root/.ssh) || chcon --user=system_u --role=object_r --type=net_conf_t --range=s0 /etc/hosts

}

function bashrc_setting() {
  backup_single_file ~/.bashrc

  # history related
  # HISTTIMEFORMAT

  # PROMPT_COMMAND
  # Refer: http://dl528888.blog.51cto.com/2382721/1703059
  # Refer: http://www.tldp.org/HOWTO/Bash-Prompt-HOWTO/x264.html
  # TODO(Guodong Ding) a known issue, it will make user confused when a command line include a double quotes("), because is not a valid json any more
  #   like this "{"TIME":"2016-05-18 15:43:14", "HOSTNAME":"chris.51devops.com", "IP":"10.20.0.1", "LOGIN":"root", "USER":"root", "CMD":"export PROMPT_COMMAND='history 1|tail -1|sed "s/^[ ]\+[0-9]\+  //"|sed "s/^/{/"|sed "s/$/\"}/">> /var/log/command.log'"}"

  if ! grep HISTTIMEFORMAT ~/.bashrc && ! grep PROMPT_COMMAND; then
    cat >>~/.bashrc <<eof
export HISTTIMEFORMAT="\"TIME\":\"%F %T\", \"HOSTNAME\":\"$HOSTNAME\", \"IP\":\"\$(who -u am i 2>/dev/null| awk '{print \$NF}'|sed -e 's/[()]//g')\", \"LOGIN\":\"\$(who am i|awk '{print \$1}')\", \"USER\":\"\${USER}\", \"CMD\":\""
export PROMPT_COMMAND='history 1|tail -1|sed "s/^[ ]\+[0-9]\+  //"|sed "s/^/{/"|sed "s/$/\"}/">> /var/log/.command_history.log'
eof
  fi
}

function update_local_time() {
  if ! grep -qa CST-8 /etc/localtime || ! diff /etc/localtime /usr/share/zoneinfo/Asia/Shanghai >/dev/null 2>&1; then
    rm -rf /etc/localtime
    ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
  fi
  ntpdate -u pool.ntp.org >/dev/null 2>&1 || ntpdate -u time.nist.gov >/dev/null 2>&1 || ntpdate -u time-nw.nist.gov >/dev/null 2>&1
  date
  cat >>/etc/rc.local <<eof
ntpdate -u pool.ntp.org || ntpdate -u time.nist.gov || ntpdate -u time-nw.nist.gov
hwclock -w
eof
  # Recommended do
  touch /etc/cron.daily/ntpdate
  chown -R --reference=/etc/cron.daily/logrotate /etc/cron.daily/ntpdate
  chmod -R --reference=/etc/cron.daily/logrotate /etc/cron.daily/ntpdate
  chcon -R --reference=/etc/cron.daily/logrotate /etc/cron.daily/ntpdate >/dev/null 2>&1
  cat >>/etc/cron.daily/ntpdate <<eof
ntpdate -u pool.ntp.org >/dev/null 2>&1 || ntpdate -u time.nist.gov >/dev/null 2>&1 || ntpdate -u time-nw.nist.gov >/dev/null 2>&1
hwclock -w
eof

}

function system_performance_tuning() {
  cp /etc/security/limits.conf "/etc/security/limits.conf_origin_$(date +%Y%m%d%H%M%S)~"
  cat >>/etc/security/limits.conf <<eof
# refer to the free inodes number, by "df -i" commands
* soft nofile 200000
* hard nofile 200000

eof

  cp /etc/sysctl.conf "/etc/sysctl.conf_origin_$(date +%Y%m%d%H%M%S)~"
  cat >/etc/sysctl.conf <<eof
fs.file-max = 808127
kernel.core_uses_pid = 1
kernel.hung_task_timeout_secs = 0
kernel.msgmax = 65536
kernel.msgmnb = 65536
kernel.sem = 250 32000 100 128
kernel.shmall = 4294967296
kernel.shmmax = 536870912
kernel.shmmax = 68719476736
kernel.shmmni = 4096
kernel.sysrq = 0
net.core.netdev_max_backlog = 262144
net.core.rmem_default = 8388608
net.core.rmem_max = 16777216
net.core.somaxconn = 262144
net.core.wmem_default = 8388608
net.core.wmem_max = 16777216
net.ipv4.conf.default.accept_source_route = 0
net.ipv4.conf.default.rp_filter = 1
net.ipv4.ip_forward = 0
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_fin_timeout = 1
net.ipv4.tcp_keepalive_time = 1200
net.ipv4.tcp_max_orphans = 3276800
net.ipv4.tcp_max_syn_backlog = 262144
net.ipv4.tcp_max_tw_buckets = 6000
net.ipv4.tcp_mem = 94500000 915000000 927000000
net.ipv4.tcp_rmem = 4096 87380 4194304
net.ipv4.tcp_sack = 1
net.ipv4.tcp_synack_retries = 5
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_syn_retries = 5
net.ipv4.tcp_timestamps = 0
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_window_scaling = 1
net.ipv4.tcp_wmem = 4096 16384 4194304
vm.swappiness = 30
eof
  # TODO(Guodong Ding) OOM(sysctl -a | grep oom)

  # TODO(Guodong Ding) 5% reserved-blocks-percentage(tune2fs, dump2fs, man tune2fs)

  # TODO(Guodong Ding) Forward & redirects
  #net.ipv4.ip_forward = 1
  #net.ipv4.ip_forward_use_pmtu = 0
  #net.ipv4.conf.default.accept_source_route = 0
  #net.ipv4.conf.all.rp_filter = 2
  #net.ipv4.conf.default.rp_filter = 2

  #net.ipv4.conf.all.accept_redirects = 1
  #net.ipv4.conf.default.accept_redirects = 1
  #net.ipv4.conf.all.secure_redirects = 1
  #net.ipv4.conf.default.secure_redirects = 1
  #net.ipv4.conf.all.send_redirects = 1
  #net.ipv4.conf.default.send_redirects = 1

  sysctl -p >/dev/null 2>&1

}

# TODO(Guodong Ding) noatime(man mount), fstab is only read by programs, and not written; it is the duty of the system \
#     administrator to properly create and maintain this file.
#     rw, suid, dev, exec, auto, nouser, async, and noatime

# TODO(Guodong Ding) more points

function ssh_config() {
  if grep "^#UseDNS" /etc/ssh/sshd_config >/dev/null 2>&1 && ! grep "UseDNS no" /etc/ssh/sshd_config >/dev/null 2>&1; then
    cp /etc/ssh/sshd_config "/etc/ssh/sshd_config_origin_$(date +%Y%m%d%H%M%S)~"
    sed -i "s/^#UseDNS.*.$/UseDNS no/g" /etc/ssh/sshd_config
    #
    sed -i 's/GSSAPIAuthentication yes/GSSAPIAuthentication no/' /etc/ssh/sshd_config
    service sshd restart
  fi

}

# TODO(Guodong Ding) Reducing Disk IO By Mounting Partitions With noatime
# Refer: https://www.howtoforge.com/reducing-disk-io-by-mounting-partitions-with-noatime
# Refer: man 8 mount

function add_user_as_user_defined() {
  if test -z ${user_defined_username}; then
    return 0
  else
    useradd ${user_defined_username}
    test -d /etc/sudoers.d && echo "$user_defined_username ALL=(ALL) NOPASSWD: ALL" >/etc/sudoers.d/user_${user_defined_username}.conf
  fi

}

function initialize() {
  check_network_connectivity
  check_name_resolve
  set_hostname_fqdn_format
  yum_install_base_packages
  yum_repository_config
  yum_install_extra_packages
  customized_commands
  inject_ssh_key_for_root
  bashrc_setting
  update_local_time
  system_performance_tuning
  ssh_config
}
function main() {
  lock_filename="lock_$$_$RANDOM"
  #    lock_filename_full_path="/var/lock/subsys/$lock_filename"
  lock_filename_full_path="/var/lock/$lock_filename"
  if (
    set -o noclobber
    echo "$$" >"$lock_filename_full_path"
  ) 2>/dev/null; then
    trap 'rm -f "$lock_filename_full_path"; exit $?' INT TERM EXIT
    # Just a test for call itself, comment it
    if [[ $# -ne 1 ]]; then
      [ ! -x "${WORKDIR}"/"$(basename "$0")" ] && chmod +x "${WORKDIR}"/"$(basename "$0")"
      test -z "${header}" || echo_b "$header"
      "${WORKDIR}"/"$(basename "$0")" initialize
      exit 0
    fi
    case $1 in
    initialize)
      initialize
      ;;
    help | *)
      test -z "${header}" || echo_b "$header"
      echo "Usage: $0 {initialize} with $0 itself"
      exit 1
      ;;
    esac

    rm -f "$lock_filename_full_path"
    trap - INT TERM EXIT
  else
    echo "Failed to acquire lock: $lock_filename_full_path"
    echo "held by $(cat ${lock_filename_full_path})"
  fi

}

main "$*"

# debug option
#${_XTRACE_FUNCTIONS}
