#!/bin/bash
# Copyright 2014 Quobyte Inc.

# Script to generate a service certificate using an existing CA.

trap onexit EXIT
function onexit() {
  if [ -f "$OPENSSL_CONFIG" ]; then
    rm "$OPENSSL_CONFIG"
  fi
  if [ -f "$OPENSSL_OUTPUT" ]; then
    rm "$OPENSSL_OUTPUT"
  fi
  if [ -f "$PUSH_SCRIPT" ]; then
    rm "$PUSH_SCRIPT"
  fi
}

QUOBYTE_SERVICES=(registry metadata data api webconsole s3 nfs client)
function print_usage_and_exit {
  for (( i = 0 ; i < ${#QUOBYTE_SERVICES[@]} ; i++ )); do
    services="${services}${QUOBYTE_SERVICES[i]} "
  done

  printf "Usage: %s --type <service_type> --host <hostname> [--ca <directory>] [--ssh-user <user>] [--no-push]\n" $(basename $0)
cat <<EOF

Generates the configuration for a new service of 'service_type' running at
'hostname' and automatically pushes it to 'hostname' using SSH and sudo.

Expects that 'quobyte_init_ca' was run before.

Required Arguments:
  --type     Available service types: $services
  --host     FQDN of host where service will run.

Optional Arguments:
  --ca           Directory where "quobyte_init_ca" initialized the CA.
                 If not specified, current working directory is used.
  --ssh-user     Username used for SSH login at 'hostname'.
                 Default: current user
                 If the username is set to 'root', 'sudo' will not be used.
  --no-push      Do not push configuration to 'hostname'.

EOF
  exit 1
}

# Parse arguments.
if [ $# -eq 0 ]; then
  print_usage_and_exit
fi
BINARY_NAME="$(basename "$0")"
OPTS=`getopt -n "$BINARY_NAME" -o h -l type:,host:,help::,ca:,ssh-user:,no-push::,no-strict-host-key-checking::,tenant_id:,volume_uuid: -- "$@"`
if [ $? -ne 0 ]; then
    echo "ERROR: Failed to parse command line options."
    exit 2
fi

unset -v SERVICE_TYPE
unset -v SERVICE_HOSTNAME
unset -v CA_DIRECTORY
unset -v HELP
unset -v SSH_USER
unset -v NO_PUSH
unset -v NO_STRICT_HOST_KEY_CHECKING
unset -v USER_DOMAINS
unset -v VOLUME_UUID
eval set -- "$OPTS"
while true
do
    case "$1" in
        --type) SERVICE_TYPE=$2; shift 2;;
        --host) SERVICE_HOSTNAME=$2; shift 2;;
        --ca) CA_DIRECTORY=$2; shift 2;;
        --ssh-user) SSH_USER=$2; shift 2;;
        --no-push) NO_PUSH=1; shift 2;;
        --no-strict-host-key-checking) NO_STRICT_HOST_KEY_CHECKING=1; shift 2;;
        -h) HELP=1; shift;;
        --help) HELP=1; shift 2;;
        --tenant_id) USER_DOMAINS+=($2); shift 2;;
        --volume_uuid) VOLUME_UUID=$2; shift 2;;
        --) shift; break;;
    esac
done
if [ -n "$HELP" ]; then
  print_usage_and_exit
fi

# Check options.
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
if [ "$(dirname `readlink --canonicalize "$0"`)" = "/usr/bin" ]; then
  source "/opt/quobyte/lib/quobyte_ca_lib.inc"
else
  source "${SCRIPT_DIR}/quobyte_ca_lib.inc"
fi
if [ $? -ne 0 ]; then
  echo "ERROR: Failed to include required library."
  exit 3
fi
check_ca_directory

# Init variables.
OPENSSL_OUTPUT="$(mktemp --suffix .quobyte_create_service_openssl_output.txt)"
OPENSSL_CONFIG="$(mktemp --suffix .quobyte_create_service_openssl_config.txt)"
if [ "$(dirname `readlink --canonicalize "$0"`)" = "/usr/bin" ]; then
  OPENSSL_TEMPLATE="/usr/share/quobyte/openssl-template.conf"
else
  OPENSSL_TEMPLATE="${SCRIPT_DIR}/openssl-template.conf"
fi

# Main.
check_ca_initialized_or_exit

if [ -z "$SERVICE_TYPE" ]; then
  print_usage_and_exit
fi
if [ -z "$SERVICE_HOSTNAME" ]; then
  print_usage_and_exit
fi
if [ ${#USER_DOMAINS[@]} -gt 1 ]; then
  echo "ERROR: --tenant_id must not be specified multiple times."
  exit 4
fi
test_for_illegal_characters_and_exit "${USER_DOMAINS[@]}"

service_found=0
service_type_lc=$(echo $SERVICE_TYPE | tr '[:upper:]' '[:lower:]')
for (( i = 0 ; i < ${#QUOBYTE_SERVICES[@]} ; i++ )); do
  if [[ $service_type_lc == ${QUOBYTE_SERVICES[i]} ]]; then
    service_found=1
    break
  fi
done
if [ $service_found -ne 1 ]; then
  echo "ERROR: Unknown service type: ${SERVICE_TYPE}"
  echo
  print_usage_and_exit
fi
if [ "$service_type_lc" = "client" ] || [ "$service_type_lc" = "nfs" ]; then
  domain="default"
  if [ -z "$3" ]; then
    domain="$3"
  fi
fi

test_for_uuid_and_exit "$VOLUME_UUID"

if [ -n "$VOLUME_UUID" -a ${#USER_DOMAINS[@]} -lt 1 ]; then
  echo "ERROR: You need to specify --tenant_id when using --volume_uuid"
  exit 4
fi

base_filename="service_${SERVICE_TYPE}_${SERVICE_HOSTNAME}"
SERVICE_CSR="${CA_DIRECTORY}/${base_filename}.csr"
SERVICE_KEY="${CA_DIRECTORY}/${base_filename}.key"
SERVICE_PEM="${CA_DIRECTORY}/${base_filename}.pem"
SERVICE_P12="${CA_DIRECTORY}/${base_filename}.p12"
SERVICE_CFG="${CA_DIRECTORY}/${base_filename}.cfg"
if [ "$SERVICE_TYPE" = "nfs" ]; then
  SERVICE_CFG2="$SERVICE_CFG"
  SERVICE_CFG=${SERVICE_CFG/_nfs_/_nfs-adapter_}
fi
if [ -e "$SERVICE_CSR" -o -e "$SERVICE_KEY" -o -e "$SERVICE_PEM" -o -e "$SERVICE_P12" -o -e "$SERVICE_CFG" ]; then
  echo "ERROR: Some (certificate) files already exist in the ${CA_DIRECTORY_TEXT}. To re-create them, delete all files matching ${base_filename}.* first."
  exit 4
fi


UUID=`uuidgen 2> /dev/null`
if [ -z "$UUID" ]; then
  echo "ERROR: Failed to generate a random UUID using 'uuidgen'."
  exit 5
fi

create_service_cfg_from_template_or_exit "$service_type_lc" "$SERVICE_CFG"

openssl genrsa -out "$SERVICE_KEY" $KEY_LENGTH \
  >"$OPENSSL_OUTPUT" 2>&1
check_openssl_return_code_or_exit $? $OPENSSL_OUTPUT

export cn=$(encode_service_type "$SERVICE_TYPE")";${UUID}"
if [ "$service_type_lc" = "client" ] || [ "$service_type_lc" = "nfs" ]; then
  ou_count=1
  for domain in "${USER_DOMAINS[@]}"
  do
    export optional_ou="${optional_ou}"$'\n'"${ou_count}.OU = D;${domain}"
    ou_count=$(($ou_count + 1))
    echo "WARNING: Access restrictions encoded in the certificate are deprecated and will no longer work in future releases"
  done
  if [ -n "$VOLUME_UUID" ]; then
    echo "WARNING: Access restrictions encoded in the certificate are deprecated and will no longer work in future releases"
    export optional_ou="${optional_ou}"$'\n'"${ou_count}.OU = V;${VOLUME_UUID}"
  fi
fi
export hostname=$SERVICE_HOSTNAME
envsubst < "$OPENSSL_TEMPLATE" > "$OPENSSL_CONFIG"
openssl req -new -nodes \
  -key "$SERVICE_KEY" \
  -out "$SERVICE_CSR" \
  -config "$OPENSSL_CONFIG" \
  >"$OPENSSL_OUTPUT" 2>&1
check_openssl_return_code_or_exit $? "$OPENSSL_OUTPUT" "$OPENSSL_CONFIG"

openssl x509 -CA "$CA_PEM" -CAkey "$CA_KEY" \
  -CAserial "$CA_SRL" -req \
  -in "$SERVICE_CSR" \
  -out "$SERVICE_PEM" -days $EXPIRATION_TIME \
  -extensions v3_req -extfile "$OPENSSL_CONFIG" \
  >"$OPENSSL_OUTPUT" 2>&1
check_openssl_return_code_or_exit $? "$OPENSSL_OUTPUT" "$OPENSSL_CONFIG"

openssl pkcs12 -export -in "$SERVICE_PEM" -inkey "$SERVICE_KEY" \
  -certfile "$CA_PEM" \
  -out "$SERVICE_P12" -nodes -passout pass: \
  -name "${base_filename}" \
  >"$OPENSSL_OUTPUT" 2>&1
check_openssl_return_code_or_exit $? $OPENSSL_OUTPUT

# Always bundle the CA with the client and nfs config because the host.cfg is not (fully) parsed by them.
if [ "$SERVICE_TYPE" = "client" ] || [ "$SERVICE_TYPE" = "nfs" ]; then
  echo "<$CA_OPTION>"   >> "$SERVICE_CFG"
  cat "$CA_PEM"         >> "$SERVICE_CFG"
  echo "</$CA_OPTION>"  >> "$SERVICE_CFG"
fi

echo "<$CERT_OPTION>"   >> "$SERVICE_CFG"
cat "$SERVICE_PEM"      >> "$SERVICE_CFG"
echo "</$CERT_OPTION>"  >> "$SERVICE_CFG"

echo "<$KEY_OPTION>"    >> "$SERVICE_CFG"
cat "$SERVICE_KEY"      >> "$SERVICE_CFG"
echo "</$KEY_OPTION>"   >> "$SERVICE_CFG"

if [ "$SERVICE_TYPE" != "client" ]; then
  cat <<EOF >> "$SERVICE_CFG"
# Service UUID
uuid=$UUID
EOF
fi

cat <<EOF
Created service configuration at: ${SERVICE_CFG}
EOF

if [ -z "$QUOBYTE_ETC_DIR" ]; then
  QUOBYTE_ETC_DIR="/etc/quobyte"
fi
host_cfg_remote="host.cfg"
host_cfg_content=`cat $HOST_CFG`
if [ -z "$host_cfg_content" ]; then
  echo "ERROR: Failed to read ${HOST_CFG}."
  exit 6
fi
service_cfg_remote="${service_type_lc}.cfg"
service_cfg_content=`cat $SERVICE_CFG`
if [ -z "$service_cfg_content" ]; then
  echo "ERROR: Failed to read ${SERVICE_CFG}."
  exit 7
fi
if [ "$SERVICE_TYPE" = "nfs" ]; then
  service_cfg_remote="nfs-adapter.cfg"
  service_cfg_remote2="nfs.cfg"
  service_cfg_content2=`cat $SERVICE_CFG2`
  if [ -z "$service_cfg_content2" ]; then
    echo "ERROR: Failed to read ${SERVICE_CFG2}."
    exit 7
  fi
fi

if [ -z "$NO_PUSH" ]; then
  PUSH_SCRIPT="$(mktemp --suffix .quobyte_create_service_push_script.sh)"
  if [ -z "$SSH_USER" ]; then
    SSH_USER="$USER"
  fi
  if [ "$SSH_USER" = "root" ]; then
    BASH_SHELL="bash"
  else
    BASH_SHELL="sudo bash"
  fi
  if [ -n "$NO_STRICT_HOST_KEY_CHECKING" ]; then
    SSH_OPTIONS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
  fi
  if [ -z "$QUOBYTE_SCP" ]; then
    QUOBYTE_SCP="scp -q $SSH_OPTIONS $PUSH_SCRIPT $SSH_USER@$SERVICE_HOSTNAME:$PUSH_SCRIPT "
  fi
  if [ -z "$QUOBYTE_SSH" ]; then
    QUOBYTE_SSH="ssh -t -t $SSH_OPTIONS $SSH_USER@$SERVICE_HOSTNAME $BASH_SHELL"
  fi
    cat <<EOF

Pushing service configuration to host: $SERVICE_HOSTNAME via SSH as user: $SSH_USER
EOF

  cat >"$PUSH_SCRIPT" << EOF
#!/bin/bash
  errors=0
  if [ ! -d '${QUOBYTE_ETC_DIR}' ]; then
    mkdir -p '${QUOBYTE_ETC_DIR}'
    if [ \$? -ne 0 ]; then
      echo 'ERROR: ${SERVICE_HOSTNAME}: Failed to create directory ${QUOBYTE_ETC_DIR}.'
      exit 21
    fi
  fi
  if [ ! -w '${QUOBYTE_ETC_DIR}' ]; then
    echo 'ERROR: ${SERVICE_HOSTNAME}: Directory ${QUOBYTE_ETC_DIR} is not writable.'
    exit 22
  fi

  function quobyte_write_file() {
    f="${QUOBYTE_ETC_DIR}/\$1"
    content="\$2"
    local i=0
    while [ -e "\${f}" ]
    do
      if [ \$i -eq 0 ]; then
        f_bak=\${f}.bak
      else
        f_bak=\${f}.bak.\$i
      fi
      if [ -e "\${f_bak}" ]; then
        let i="\$i + 1"
        continue
      fi
      mv "\${f}" "\${f_bak}"
      if [ \$? -ne 0 ]; then
        echo "ERROR: ${SERVICE_HOSTNAME}: Failed to create backup for file: \${f}."
        exit 23
      fi
      break
    done

    # Write file.
    echo -e "\${content}" > "\${f}"
    let errors="\$errors + \$?"

    # Remove backup when both files are identical.
    if [ -n "\${f_bak}" ]; then
      diff -q "\${f}" "\${f_bak}" 2>&1 >/dev/null
      if [ \$? -eq 0 ]; then
        rm "\${f_bak}"
      fi
    fi
  }
  quobyte_write_file ${host_cfg_remote} '${host_cfg_content}'
  chmod 0644 ${host_cfg_remote}
  quobyte_write_file ${service_cfg_remote} '${service_cfg_content}'
  chmod 0640 ${service_cfg_remote}
  if [ -n '${service_cfg_remote2}' -a -n '${service_cfg_content2}' ]; then
    quobyte_write_file ${service_cfg_remote2} '${service_cfg_content2}'
  fi

  rm "\$0"

  exit \$errors
EOF

  $QUOBYTE_SCP
  if [ $? -ne 0 ]; then
    echo "ERROR: Failed to copy the push script to the remote host. Used command: $QUOBYTE_SCP"
    exit 9
  fi

  $QUOBYTE_SSH $PUSH_SCRIPT
  if [ $? -eq 0 ]; then
    cat <<EOF

Pushed service configuration to host: $SERVICE_HOSTNAME
EOF
  else
    echo "ERROR: Failed to push files ${host_cfg_remote} and "$(basename ${SERVICE_CFG})" (as ${service_cfg_remote}) to host: ${SERVICE_HOSTNAME}"
    exit 8
  fi
else
  cat <<EOF

Not pushing the service configuration.

Copy it manually to machine: ${SERVICE_HOSTNAME} as file: ${QUOBYTE_ETC_DIR}/${service_cfg_remote}
EOF
  if [ -n "${SERVICE_CFG2}" -a -n "${service_cfg_remote2}" ]; then
    echo "Additionally, copy the file: $SERVICE_CFG2 as file: ${QUOBYTE_ETC_DIR}/${service_cfg_remote2}"
  fi
fi

if [ "$service_type_lc" = "dir" ]; then
  cat <<EOF

Once you started the new Registry (DIR) service, we recommend to update
the SRV DNS record which has the list of Registry (DIR) services.
Please see the manual how to configure SRV DNS records.
EOF
fi
