dev/ipa-cluster.bash (248 lines of code) (raw):

#!/usr/bin/env bash set -o errexit WORKING_DIR="$(pwd)" # Follow symlinks to find the real script cd "$(dirname "$0")" || exit 1 script_file=$(pwd)/$(basename "$0") while [[ -L "$script_file" ]]; do script_file=$(readlink "$script_file") cd "$(dirname "$script_file")" || exit 1 script_file=$(pwd)/$(basename "$script_file") done parent_script_dir="$( (cd "$( dirname "${script_file}" )/.." && pwd -P) )" cd "$WORKING_DIR" || exit 1 # setup common global variables export TF_VAR_env_name=${TF_VAR_env_name:-${USER}} cluster_name=${TF_VAR_env_name} terraform_dir="${parent_script_dir}/concourse/terraform/ipa-multinode-hadoop" ansible_play_path="${parent_script_dir}/concourse/ansible/ipa-multinode-hadoop" metadata_path="${terraform_dir}/output.json" store_password="$(env | awk -F= '/ANSIBLE_VAR_ssl_store_password/{print $2}')" # ANSI Colors echoRed() { echo $'\e[0;31m'"$1"$'\e[0m'; } echoGreen() { echo $'\e[0;32m'"$1"$'\e[0m'; } echoYellow() { echo $'\e[0;33m'"$1"$'\e[0m'; } function printUsage() { local normal bold normal=$(tput sgr0) bold=$(tput bold) cat <<-EOF ${bold}NAME${normal} ipa-cluster.bash - manage a multi-node HA Hadoop cluster secured by IPA in GCP ${bold}Usage${normal}: ipa-cluster.bash <command> ipa-cluster.bash --create - to create a new IPA cluster in GCP ipa-cluster.bash --destroy - to destroy an existing IPA cluster in GCP EOF } function fail() { cd "$WORKING_DIR" echoRed "ERROR: $1" exit 1 } function check_pre_requisites() { if ! type terraform &>/dev/null; then fail 'terraform is not found, did you install it (e.g. "brew install terraform") ?' fi if ! type jq &>/dev/null; then fail 'jq is not found, did you install it (e.g. "brew install jq") ?' fi if ! type ansible-playbook &>/dev/null; then fail 'ansible-playbook is not found, did you install it ?' fi if ! type xmlstarlet &>/dev/null; then fail 'xmlstarlet is not found, did you install it (e.g. "brew install xmlstarlet") ?' fi : "${PXF_BASE?"PXF_BASE is required. export PXF_BASE=[YOUR_PXF_CONFIG_LOCATION]"}" : "${TF_VAR_gcp_project?"TF_VAR_gcp_project is required. export TF_VAR_gcp_project=[YOUR_GCP_PROJECT_NAME]"}" : "${ANSIBLE_VAR_ipa_password?"ANSIBLE_VAR_ipa_password is required. export ANSIBLE_VAR_ipa_password=[YOUR_IPA_PASSWORD]"}" : "${ANSIBLE_VAR_ssl_store_password?"ANSIBLE_VAR_ssl_store_password is required. export ANSIBLE_VAR_ssl_store_password=[YOUR_SSL_STORE_PASSWORD]"}" } function create_firewall_resource() { local my_ip=$(curl ifconfig.co) cat << EOF > "${terraform_dir}"/local-firewall.tf resource "google_compute_firewall" "$USER-access" { name = "$USER-access" network = var.network direction = "INGRESS" allow { protocol = "all" } source_ranges = ["${my_ip}/32"] } EOF } function create_override_file() { # create an override file that opens up public IP for the nodes provisioned by terraform cat << EOF > "${terraform_dir}"/override.tf resource "google_compute_instance" "ipa" { network_interface { subnetwork = var.subnet access_config { // Ephemeral public IP } } } resource "google_compute_instance" "namenode" { network_interface { subnetwork = var.subnet access_config { // Ephemeral public IP } } } resource "google_compute_instance" "datanode" { network_interface { subnetwork = var.subnet access_config { // Ephemeral public IP } } } EOF } # apply terraform template to provision the cluster function terraform_apply() { cd "${terraform_dir}" || fail "cannot go to ${terraform_dir}" terraform init create_firewall_resource create_override_file terraform apply -auto-approve -var-file="${terraform_dir}/local.tfvars" terraform output -json > "${terraform_dir}/output.json" } # destroy resources provisioned by terraform function terraform_destroy() { cd "${terraform_dir}" || fail "cannot go to ${terraform_dir}" terraform destroy -var-file="${terraform_dir}/local.tfvars" } # cleanup ansible artifacts that might exist from previous runs function cleanup_existing_artifacts() { rm "${ansible_play_path}"/*.p12 rm "${ansible_play_path}"/inventory.ini rm "${ansible_play_path}"/config.yml } function generate_keystores() { # generate pkcs12 keystore for NameNode VMs num_namenodes=$(jq <"${metadata_path}" -r '.namenode_tls_cert.value | length') for i in $(seq 0 $((num_namenodes - 1))); do cert="$(jq <"${metadata_path}" -r ".namenode_tls_cert.value[$i].cert_pem")" key="$(jq <"${metadata_path}" -r ".namenode_tls_private_key.value[$i].private_key_pem")" name="$(jq <"${metadata_path}" -r ".namenode_tls_cert.value[$i].subject[0].common_name")" openssl pkcs12 -export -in <(echo "${cert}") -inkey <(echo "${key}") -out "${ansible_play_path}/${name%%.*}.p12" -name "${name}" -password "pass:${store_password}" echo "${cert}" | keytool -noprompt -import -trustcacerts -alias "${name}" -keystore "${ansible_play_path}/truststore.p12" -storetype pkcs12 -storepass "${store_password}" done # generate pkcs12 keystore for DataNode VMs num_datanodes=$(jq <"${metadata_path}" -r '.datanode_tls_cert.value | length') for i in $(seq 0 $((num_datanodes - 1))); do cert="$(jq <"${metadata_path}" -r ".datanode_tls_cert.value[$i].cert_pem")" key="$(jq <"${metadata_path}" -r ".datanode_tls_private_key.value[$i].private_key_pem")" name="$(jq <"${metadata_path}" -r ".datanode_tls_cert.value[$i].subject[0].common_name")" openssl pkcs12 -export -in <(echo "${cert}") -inkey <(echo "${key}") -out "${ansible_play_path}/${name%%.*}.p12" -name "${name}" -password "pass:${store_password}" echo "${cert}" | keytool -noprompt -import -trustcacerts -alias "${name}" -keystore "${ansible_play_path}/truststore.p12" -storetype pkcs12 -storepass "${store_password}" done } # setup ssh_config with host alias, users, and identity files function setup_ssh_config() { mkdir -p ~/.ssh jq <"${metadata_path}" -r '.ssh_config.value' >>~/.ssh/config jq <"${metadata_path}" -r '.private_key.value' >~/.ssh/ipa_"${cluster_name}"_rsa chmod 0600 ~/.ssh/ipa_"${cluster_name}"_rsa ssh-keygen -y -f ~/.ssh/ipa_"${cluster_name}"_rsa >~/.ssh/ipa_"${cluster_name}"_rsa.pub } # run ansible playbook to configure Hadoop on the cluster function run_ansible_playbook() { # generate ansible configuration files jq <"${metadata_path}" -r '.ansible_inventory.value' >"${ansible_play_path}"/inventory.ini jq <"${metadata_path}" -r '.ansible_variables.value' >"${ansible_play_path}"/config.yml # append additional (sensitive) variables from the enironment env | sed -e '/^ANSIBLE_VAR_/!d;s/ANSIBLE_VAR_\(.*\)=\(.*\)/\1: \2/' >>"${ansible_play_path}"/config.yml pushd "${ansible_play_path}" || fail "cannot go to ${ansible_play_path}" ansible-galaxy collection install -r requirements.yml ansible-playbook main.yml popd || fail "cannot come out of ${ansible_play_path}" } # create / gather environment files under ipa_env_files to use with PXF function create_environment_files() { hadoop_namenode="ccp-${cluster_name}-nn01" ipa_server="ccp-${cluster_name}-ipa" hive_node="ccp-${cluster_name}-nn02" rm -rf ipa_env_files mkdir -p ipa_env_files jq <"${metadata_path}" -r ".etc_hosts.value" >ipa_env_files/etc_hostfile echo "${hadoop_namenode}" >ipa_env_files/nn01 echo "${hive_node}" >ipa_env_files/nn02 mkdir -p ipa_env_files/conf scp "${hadoop_namenode}:\$HADOOP_PREFIX/etc/hadoop/*-site.xml" ipa_env_files/conf/ scp "${hive_node}:\$HIVE_HOME/conf/hive-site.xml" ipa_env_files/conf/ cp ~/.ssh/ipa_"${cluster_name}"_rsa ipa_env_files/google_compute_engine cp ~/.ssh/ipa_"${cluster_name}"_rsa.pub ipa_env_files/google_compute_engine.pub gcp_project="$(jq <"${metadata_path}" -r ".project.value")" domain_name_lower="c.${gcp_project}.internal" domain_name_upper=$(tr '[:lower:]' '[:upper:]' <<< "$domain_name_lower") echo "$domain_name_upper" >ipa_env_files/REALM cat <<EOF >ipa_env_files/krb5_realm ${domain_name_upper} = { kdc = ${ipa_server}.${domain_name_lower} admin_server = ${ipa_server}.${domain_name_lower} } EOF cat <<EOF >ipa_env_files/krb5_domain_realm .${domain_name_lower} = ${domain_name_upper} EOF scp "${ipa_server}":~/hadoop.user.keytab ipa_env_files/ scp "${ipa_server}":~/pxf.service.keytab ipa_env_files/ # list environment files find ipa_env_files -type f } # setup PXF servers 'hdfs-ipa' and 'hdfs-ipa-no-impersonation-no-svcuser' in PXF_BASE function setup_pxf_server() { rm -rf "${PXF_BASE}"/servers/hdfs-ipa mkdir -p "${PXF_BASE}"/servers/hdfs-ipa cp ipa_env_files/conf/*.xml "${PXF_BASE}"/servers/hdfs-ipa/ cp ipa_env_files/*.keytab "${PXF_BASE}"/servers/hdfs-ipa/ cp "${parent_script_dir}"/server/pxf-service/src/templates/templates/pxf-site.xml "${PXF_BASE}"/servers/hdfs-ipa/ sed -i '' -e "s|>gpadmin/_HOST@EXAMPLE.COM<|>porter@${domain_name_upper}<|g" "${PXF_BASE}"/servers/hdfs-ipa/pxf-site.xml sed -i '' -e 's|/keytabs/|/servers/hdfs-ipa/|g' "${PXF_BASE}"/servers/hdfs-ipa/pxf-site.xml # set Hadoop client to use hostnames for datanodes instead of IP addresses (which are internal in GCP network) xmlstarlet ed --inplace --pf --append '/configuration/property[last()]' --type elem -n property -v "" \ --subnode '/configuration/property[last()]' --type elem -n name -v "dfs.client.use.datanode.hostname" \ --subnode '/configuration/property[last()]' --type elem -n value -v "true" "${PXF_BASE}"/servers/hdfs-ipa/hdfs-site.xml # set constrained delegation property to true for the PXF server xmlstarlet ed --inplace --pf --update "/configuration/property[name = 'pxf.service.kerberos.constrained-delegation']/value" -v true "${PXF_BASE}"/servers/hdfs-ipa/pxf-site.xml rm -rf "${PXF_BASE}"/servers/hdfs-ipa-no-impersonation cp -R "${PXF_BASE}"/servers/hdfs-ipa "${PXF_BASE}"/servers/hdfs-ipa-no-impersonation sed -i '' -e 's|hdfs-ipa|hdfs-ipa-no-impersonation|g' "${PXF_BASE}"/servers/hdfs-ipa-no-impersonation/pxf-site.xml # set impersonation property to false for the PXF server xmlstarlet ed --inplace --pf --update "/configuration/property[name = 'pxf.service.user.impersonation']/value" -v false "${PXF_BASE}"/servers/hdfs-ipa-no-impersonation/pxf-site.xml # set service user to foobar xmlstarlet ed --inplace --pf --append '/configuration/property[last()]' --type elem -n property -v "" \ --subnode '/configuration/property[last()]' --type elem -n name -v "pxf.service.user.name" \ --subnode '/configuration/property[last()]' --type elem -n value -v "foobar" "${PXF_BASE}"/servers/hdfs-ipa-no-impersonation/pxf-site.xml rm -rf "${PXF_BASE}"/servers/hdfs-ipa-no-impersonation-no-svcuser cp -R "${PXF_BASE}"/servers/hdfs-ipa "${PXF_BASE}"/servers/hdfs-ipa-no-impersonation-no-svcuser sed -i '' -e 's|hdfs-ipa|hdfs-ipa-no-impersonation-no-svcuser|g' "${PXF_BASE}"/servers/hdfs-ipa-no-impersonation-no-svcuser/pxf-site.xml # set impersonation property to false for the PXF server xmlstarlet ed --inplace --pf --update "/configuration/property[name = 'pxf.service.user.impersonation']/value" -v false "${PXF_BASE}"/servers/hdfs-ipa-no-impersonation-no-svcuser/pxf-site.xml } # print instructions for the manual steps the user must perform function print_user_instructions() { echo "Cluster $USER has been created, now do the following:" echo "1. --- copy the following to your /etc/hosts :" cat ipa_env_files/etc_hostfile echo "2. --- add the following to your /etc/krb5.conf :" cat << EOF [libdefaults] default_realm = C.DATA-GPDB-UD-IPA.INTERNAL ticket_lifetime = 1d forwardable = true proxiable = true [realms] C.DATA-GPDB-UD-IPA.INTERNAL = { kdc = ccp-$USER-ipa.c.data-gpdb-ud-ipa.internal admin_server = ccp-$USER-ipa.c.data-gpdb-ud-ipa.internal } [domain_realm] .c.data-gpdb-ud-ipa.internal = C.DATA-GPDB-UD-IPA.INTERNAL c.data-gpdb-ud-ipa.internal = C.DATA-GPDB-UD-IPA.INTERNAL EOF cd "$WORKING_DIR" || fail "cannot return to $WORKING_DIR" } # --- main script logic --- script_command=$1 case ${script_command} in '--create') check_pre_requisites terraform_apply cleanup_existing_artifacts generate_keystores setup_ssh_config run_ansible_playbook create_environment_files setup_pxf_server print_user_instructions ;; '--destroy') check_pre_requisites terraform_destroy ;; *) printUsage exit 2 ;; esac exit $?