bff612ab5fd601ca74865d5def1958a4a9155c02
configuration/crontab
| ... | ... | @@ -1,3 +1,3 @@ |
| 1 | 1 | * * * * * export PATH=/bin:/usr/bin:/usr/local/bin; sleep $(( $RANDOM * 60 / 32768 )); update_authorized_keys_for_landscape_managers_if_changed $( cat /root/ssh-key-reader.token ) https://security-service.sapsailing.com /root 2>&1 >>/var/log/sailing.err |
| 2 | 2 | # NOTICE: Please try to reference the customised crontabs at $GIT_HOME/configuration/crontabs or use |
| 3 | -# the build-crontab script. This file has been maintained for continuity, but is deprecated. |
|
| ... | ... | \ No newline at end of file |
| 0 | +# the build-crontab-and-cp-files script. This file has been maintained for continuity, but is deprecated. |
|
| ... | ... | \ No newline at end of file |
configuration/crontabs/README
| ... | ... | @@ -1,7 +1,7 @@ |
| 1 | 1 | This is the crontab repo and it contains one line crontabs for all the different environments. |
| 2 | -These files are concatenated by the build_crontab script. Any time the crontab should contain |
|
| 2 | +These files are concatenated by the build-crontab-and-cp-files script. Any time the crontab should contain |
|
| 3 | 3 | a user's home directory, instead write PATH_OF_HOME_DIR_TO_REPLACE; if the crontab should |
| 4 | 4 | contain the path to the git directory, instead write PATH_OF_GIT_HOME_DIR_TO_REPLACE. |
| 5 | -These are replaced by the build-crontab script. |
|
| 5 | +These are replaced by the build-crontab-and-cp-filesscript. |
|
| 6 | 6 | |
| 7 | 7 | Note: theses files are symbolically linked to, so beware of the ramifications of changes. |
| ... | ... | \ No newline at end of file |
configuration/environments_scripts/build-crontab-and-cp-files
| ... | ... | @@ -1,13 +1,19 @@ |
| 1 | 1 | #!/bin/bash |
| 2 | 2 | |
| 3 | -# Purpose: The first parameter is an image type. This script iterates over the image types' USER folders, concatenating all the symbolic |
|
| 4 | -# links (referencing locations within configuration/crontabs) into one file for each user. It also takes the name of the user containing a copy of the git repo |
|
| 5 | -# as well as the name of the dir within that user to go to. The created files for each user go into that user's |
|
| 6 | -# home dir and are installed for that user too. Note: within these crontabs are certain regexes, which are replaced by the path to the git home dir and the matching |
|
| 7 | -# user's home. |
|
| 3 | +# Purpose: The first parameter is an image type. This script iterates over the image types' USER folders, |
|
| 4 | +# concatenating all the symbolic links (referencing locations within configuration/crontabs) into one crontab |
|
| 5 | +# file for each user. It also takes the name of the user containing a copy of the git repo as well as the name of the |
|
| 6 | +# dir within that user to go to. The created files for each user go into that user's home dir and are installed for |
|
| 7 | +# that user too. |
|
| 8 | +# Within these crontab stubs, are certain string literals, which are replaced: PATH_OF_GIT_HOME_DIR_TO_REPLACE becomes |
|
| 9 | +# the absolute path to the git home dir, and PATH_OF_HOME_DIR_TO_REPLACE is changed to the path of the home directory |
|
| 10 | +# in which the crontab will be installed. |
|
| 8 | 11 | # Useful files are also copied across from the "files" dir within each image type. |
| 12 | +# Finally, the script ends by performing a deamon reload and reenabling all the systemd units in the files directory, |
|
| 13 | +# so that the correct wants, requires, etc. are installed/linked. Any systemd units, within the mimicked filesystem, |
|
| 14 | +# will be automatically enabled. |
|
| 9 | 15 | |
| 10 | -if [[ $# -ne 3 && $# -ne 4 && $# -ne 5 ]]; then |
|
| 16 | +if [[ $# -lt 3 || $# -gt 5 ]]; then |
|
| 11 | 17 | echo "$0 <ENVIRONMENT_TYPE> <USER_WITH_COPY_OF_REPO> <RELATIVE_PATH_OF_GIT_DIR_WITHIN_USER> " |
| 12 | 18 | echo "" |
| 13 | 19 | echo "Where USER_WITH_COPY_OF_REPO is a user that contains a checked out copy of the main git." |
| ... | ... | @@ -37,6 +43,7 @@ if [[ -d "users" ]]; then |
| 37 | 43 | for dir in $(ls -d */ ); do |
| 38 | 44 | USERNAME=$(echo $dir | sed "s/\/$//") # Dirname is the username. The trailing slash is removed. |
| 39 | 45 | HOME_DIR=$(eval echo $(printf "~%q" "$USERNAME")) # The path to the home dir of the user whose cronjob will be installed. |
| 46 | + # Clear the crontab file before assembling it from the snippets: |
|
| 40 | 47 | > $HOME_DIR/crontab |
| 41 | 48 | for crontab in $(ls ${USERNAME}/crontab*); do |
| 42 | 49 | cat "${crontab}">> $HOME_DIR/crontab |
| ... | ... | @@ -53,7 +60,9 @@ fi |
| 53 | 60 | if [[ "$ONLY_CRONTAB" != "true" && -d "files" ]]; then |
| 54 | 61 | cd "files" |
| 55 | 62 | \cp -rL * / # copies all files accross, realising any symbolic links. The backslash escapes the alias cp -i. |
| 56 | -fi |
|
| 57 | - |
|
| 58 | -systemctl daemon-reload |
|
| 59 | -systemctl reenable $(find /etc/systemd/system/ -type f) # to recreate all the dependency links. |
|
| ... | ... | \ No newline at end of file |
| 0 | + systemctl daemon-reload |
|
| 1 | + for unit in $(find etc/systemd/system -type f); do |
|
| 2 | + systemctl reenable /"${unit}" # to recreate all the dependency links. |
|
| 3 | + done |
|
| 4 | + cd .. |
|
| 5 | +fi |
|
| ... | ... | \ No newline at end of file |
configuration/environments_scripts/repo/usr/local/bin/imageupgrade_functions.sh
| ... | ... | @@ -101,7 +101,7 @@ setup_keys() { |
| 101 | 101 | TEMP_KEY_DIR=$(mktemp -d /root/keysXXXXX) |
| 102 | 102 | REGION=$(TOKEN=`curl -X PUT "http://169.254.169.254/latest/api/token" --silent -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"` \ |
| 103 | 103 | && curl -H "X-aws-ec2-metadata-token: $TOKEN" --silent http://169.254.169.254/latest/meta-data/placement/region) |
| 104 | - scp -o StrictHostKeyChecking=no -pr root@sapsailing.com:/root/key_vault/${1}/* "${TEMP_KEY_DIR}" |
|
| 104 | + scp -o StrictHostKeyChecking=no -pr root@sapsailing.com:/root/key_vault/"${1}"/* "${TEMP_KEY_DIR}" |
|
| 105 | 105 | cd "${TEMP_KEY_DIR}" |
| 106 | 106 | for user in $(ls); do |
| 107 | 107 | if id -u "$user"; then |
| ... | ... | @@ -109,20 +109,23 @@ setup_keys() { |
| 109 | 109 | # aws setup |
| 110 | 110 | if [[ -d "${user}/aws" ]]; then |
| 111 | 111 | mkdir --parents "${user_home_dir}/.aws" |
| 112 | - chmod 766 "${user_home_dir}/.aws" |
|
| 113 | - chown -R ${user}:${user} "${user_home_dir}/.aws" |
|
| 112 | + chmod 755 "${user_home_dir}/.aws" |
|
| 114 | 113 | \cp -r --preserve --dereference "${user}"/aws/* "${user_home_dir}/.aws" |
| 115 | - sed -i "s/region = .*/region = ${REGION}/" "${user_home_dir}/.aws/config" |
|
| 114 | + echo "[default]" >> "${user_home_dir}/.aws/config" |
|
| 115 | + echo "region = ${REGION}" >> "${user_home_dir}"/.aws/config |
|
| 116 | + chown -R ${user}:${user} "${user_home_dir}/.aws" |
|
| 117 | + chmod 600 "${user_home_dir}"/.aws/* |
|
| 116 | 118 | fi |
| 117 | 119 | # ssh setup |
| 118 | 120 | if [[ -d "${user}/ssh" ]]; then |
| 119 | 121 | mkdir --parents "${user_home_dir}/.ssh" |
| 120 | 122 | chmod 700 "${user_home_dir}/.ssh" |
| 121 | - chown -R ${user}:${user} "${user_home_dir}/.ssh" |
|
| 122 | 123 | \cp --preserve --dereference $(find ${user}/ssh -maxdepth 1 -type f) "${user_home_dir}/.ssh" |
| 123 | 124 | for key in $(find ${user}/ssh/authorized_keys -type f); do |
| 124 | 125 | cat "${key}" >> ${user_home_dir}/.ssh/authorized_keys |
| 125 | 126 | done |
| 127 | + chown -R ${user}:${user} "${user_home_dir}/.ssh" |
|
| 128 | + chmod 600 "${user_home_dir}"/.ssh/* |
|
| 126 | 129 | fi |
| 127 | 130 | fi |
| 128 | 131 | done |
| ... | ... | @@ -179,11 +182,38 @@ setup_fail2ban() { |
| 179 | 182 | enabled = true |
| 180 | 183 | filter = sshd[mode=aggressive] |
| 181 | 184 | action = iptables[name=SSH, port=ssh, protocol=tcp] |
| 182 | - sendmail-whois[name=SSH, dest=thomasstokes@yahoo.co.uk, sender=fail2ban@sapsailing.com] |
|
| 185 | + sendmail-whois[name=SSH, dest=axel.uhl@sap.com, sender=fail2ban@sapsailing.com] |
|
| 183 | 186 | logpath = /var/log/fail2ban.log |
| 184 | 187 | maxretry = 5 |
| 185 | 188 | EOF |
| 186 | 189 | service fail2ban start |
| 187 | 190 | yum remove -y firewalld |
| 188 | - |
|
| 191 | +} |
|
| 192 | + |
|
| 193 | +setup_mail_sending() { |
|
| 194 | + yum install -y mailx postfix |
|
| 195 | + systemctl enable postfix |
|
| 196 | + temp_mail_properties_location=$(mktemp /root/mail.properties_XXX) |
|
| 197 | + scp -o StrictHostKeyChecking=no -p root@sapsailing.com:mail.properties "${temp_mail_properties_location}" |
|
| 198 | + cd $(dirname "${temp_mail_properties_location}") |
|
| 199 | + local smtp_host="$(sed -n "s/mail.smtp.host \?= \?\(.*\)/\1/p" ${temp_mail_properties_location})" |
|
| 200 | + local smtp_port="$(sed -n "s/mail.smtp.port \?= \?\(.*\)/\1/p" ${temp_mail_properties_location})" |
|
| 201 | + local smtp_user="$(sed -n "s/mail.smtp.user \?= \?\(.*\)/\1/p" ${temp_mail_properties_location})" |
|
| 202 | + local smtp_pass="$(sed -n "s/mail.smtp.password \?= \?\(.*\)/\1/p" ${temp_mail_properties_location})" |
|
| 203 | + local password_file_location="/etc/postfix/sasl_passwd" |
|
| 204 | + echo "relayhost = [${smtp_host}]:${smtp_port} |
|
| 205 | +smtp_sasl_auth_enable = yes |
|
| 206 | +smtp_sasl_security_options = noanonymous |
|
| 207 | +smtp_sasl_password_maps = hash:${password_file_location} |
|
| 208 | +smtp_use_tls = yes |
|
| 209 | +smtp_tls_security_level = encrypt |
|
| 210 | +smtp_tls_note_starttls_offer = yes |
|
| 211 | + |
|
| 212 | +myorigin =\$myhostname.sapsailing.com |
|
| 213 | +" >> /etc/postfix/main.cf |
|
| 214 | + sed -i "/smtp_tls_security_level = may/d" /etc/postfix/main.cf |
|
| 215 | + echo "[${smtp_host}]:${smtp_port} ${smtp_user}:${smtp_pass}" >> ${password_file_location} |
|
| 216 | + postmap hash:${password_file_location} |
|
| 217 | + systemctl restart postfix |
|
| 218 | + rm -f "${temp_mail_properties_location}" |
|
| 189 | 219 | } |
| ... | ... | \ No newline at end of file |
configuration/environments_scripts/repo/usr/local/bin/sync-repo-and-execute-cmd.sh
| ... | ... | @@ -17,8 +17,9 @@ COMMAND_ON_COMPLETION=$2 |
| 17 | 17 | cd ${GIT_PATH} |
| 18 | 18 | # Rev-parse gets the commit hash of given reference. |
| 19 | 19 | CURRENT_HEAD=$(git rev-parse HEAD) |
| 20 | -git fetch |
|
| 20 | +GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no" git fetch |
|
| 21 | 21 | if [[ "$?" -ne 0 ]]; then |
| 22 | + echo "Error encountered: issue with fetching or there is no repo." |
|
| 22 | 23 | exit 1 |
| 23 | 24 | fi |
| 24 | 25 | if [[ $CURRENT_HEAD != $(git rev-parse origin/main) ]] # Checks if there are new commits |
configuration/environments_scripts/reverse_proxy/files/usr/local/bin/register-deregister-from-nlb-target-group.sh
| ... | ... | @@ -3,6 +3,7 @@ |
| 3 | 3 | selfIp=$( ec2-metadata --local-ipv4 | grep -o "[0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+\>") |
| 4 | 4 | availabilityZone=$( ec2-metadata --availability-zone | grep -o "[a-zA-Z]\+-[a-zA-Z]\+-[0-9a-z]\+\>") |
| 5 | 5 | LOG_LOCATION="/var/log/registration.err" |
| 6 | +targetGroupTag="allReverseProxies" |
|
| 6 | 7 | if [[ -z "$selfIp" || -z "$availabilityZone" ]]; then |
| 7 | 8 | echo "ec2-metadata not available during nlb registration" > ${LOG_LOCATION} |
| 8 | 9 | selfIp=$(cat /var/cache/local-ip) |
| ... | ... | @@ -16,7 +17,7 @@ if [[ "$#" -eq 0 ]];then |
| 16 | 17 | fi |
| 17 | 18 | |
| 18 | 19 | addSelfToNLB() { |
| 19 | - if ! ec2-metadata --user-data | grep "image-upgrade" ; then |
|
| 20 | + if ! ec2-metadata --user-data | grep "^image-upgrade$" ; then |
|
| 20 | 21 | ec2-metadata --local-ipv4 | grep -o "[0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+\>" > /var/cache/local-ip |
| 21 | 22 | ec2-metadata --availability-zone | grep -o "[a-zA-Z]\+-[a-zA-Z]\+-[0-9a-z]\+\>" > /var/cache/availability-zone |
| 22 | 23 | aws elbv2 register-targets --target-group-arn "${targetGroupArn}" --targets Id="${selfIp}",Port=80,AvailabilityZone="${availabilityZone}" |
| ... | ... | @@ -25,15 +26,20 @@ addSelfToNLB() { |
| 25 | 26 | } |
| 26 | 27 | |
| 27 | 28 | removeSelfFromNLB() { |
| 28 | - aws elbv2 deregister-targets --target-group-arn "${targetGroupArn}" --targets Id="${selfIp}",Port=80,AvailabilityZone="${availabilityZone}" |
|
| 29 | + aws elbv2 deregister-targets --target-group-arn "${targetGroupArn}" --targets Id="${selfIp}",Port=80,AvailabilityZone="${availabilityZone}" |
|
| 29 | 30 | } |
| 30 | 31 | |
| 31 | -case $1 in |
|
| 32 | - add-to-nlb) |
|
| 33 | - addSelfToNLB |
|
| 34 | - ;; |
|
| 35 | - remove-from-nlb) |
|
| 36 | - removeSelfFromNLB |
|
| 37 | - echo "${selfIp} ${availabilityZone} ${targetGroupArn} $(date)" >> ${LOG_LOCATION} |
|
| 38 | - ;; |
|
| 39 | -esac |
|
| 32 | +for tagKey in $(aws elbv2 describe-tags --resource-arns "${targetGroupArn}" | jq -r ".TagDescriptions | .[] | .Tags| .[] | .Key "); do |
|
| 33 | + if [[ "$tagKey" == "$targetGroupTag" ]]; then |
|
| 34 | + # assumes (correctly) that AWS target group keys are unique (ie. a target group cannot have the same key twice). |
|
| 35 | + case $1 in |
|
| 36 | + add-to-nlb) |
|
| 37 | + addSelfToNLB |
|
| 38 | + ;; |
|
| 39 | + remove-from-nlb) |
|
| 40 | + removeSelfFromNLB |
|
| 41 | + echo "${selfIp} ${availabilityZone} ${targetGroupArn} $(date)" >> ${LOG_LOCATION} |
|
| 42 | + ;; |
|
| 43 | + esac |
|
| 44 | + fi |
|
| 45 | +done |
|
| ... | ... | \ No newline at end of file |
configuration/environments_scripts/reverse_proxy/files/var/www/cgi-bin/reverseProxyHealthcheck.sh
| ... | ... | @@ -1,7 +1,6 @@ |
| 1 | 1 | #!/bin/bash |
| 2 | 2 | |
| 3 | -# Purpose: This script identifies whether the instance the script runs on is in the same AZ as the archive. |
|
| 4 | -# The user it is installed on must have aws credentials that don't need mfa. Install to /usr/share/httpd/.aws. |
|
| 3 | +# Purpose: This script is the internal server status. |
|
| 5 | 4 | |
| 6 | 5 | outputMessage() { |
| 7 | 6 | # parameter 1: the message to display on the site |
| ... | ... | @@ -15,58 +14,12 @@ status() { |
| 15 | 14 | echo "Status: $1" |
| 16 | 15 | } |
| 17 | 16 | |
| 18 | -convert_ip_to_num() { |
|
| 19 | - # $1: Ip4 address in 4 octets form, separated by 3 dots. |
|
| 20 | - IFS=. read -r a b c d <<< "$1" |
|
| 21 | - echo "$(( ( $a << 24 ) + ( $b << 16 ) + ($c << 8 ) + ($d) ))" |
|
| 22 | -} |
|
| 23 | 17 | SELF_IP=$( ec2-metadata --local-ipv4 | grep -o "[0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+\>") |
| 24 | 18 | curl --silent --location --fail "http://${SELF_IP}/internal-server-status" >/dev/null |
| 25 | -if [[ "$?" -ne 0 ]]; then |
|
| 19 | +if [[ "$?" -eq 0 ]]; then |
|
| 20 | + status "200" |
|
| 21 | + outputMessage "Healthy" |
|
| 22 | +else |
|
| 26 | 23 | status "500 Reverse proxy itself is unhealthy" |
| 27 | 24 | outputMessage "Reverse proxy is unhealthy" |
| 28 | - exit 0 |
|
| 29 | -fi |
|
| 30 | -# The names of the variables in the macros file. |
|
| 31 | -ARCHIVE_IP_NAME="ARCHIVE_IP" |
|
| 32 | -ARCHIVE_FAILOVER_IP_NAME="ARCHIVE_FAILOVER_IP" |
|
| 33 | -PRODUCTION_IP_NAME="PRODUCTION_ARCHIVE" |
|
| 34 | -MACROS_PATH="/etc/httpd/conf.d/000-macros.conf" |
|
| 35 | -# The regex to extract the ip from a line ending in an ip. |
|
| 36 | -IP_REGEX="[0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+$" |
|
| 37 | -# Extracts which IP is in production. |
|
| 38 | -PRODUCTION_IP=$(sed -n -e "s/^Define ${PRODUCTION_IP_NAME}\> \(.*\)$/\1/p" ${MACROS_PATH}) |
|
| 39 | -SUBNET_MASK_SIZE_VAR_LOCATION="/var/cache/subnetMaskSize" |
|
| 40 | -if [[ ! -e "$SUBNET_MASK_SIZE_VAR_LOCATION" ]]; then |
|
| 41 | - # Get subnet mask size |
|
| 42 | - MY_AZ=$( ec2-metadata --availability-zone | grep -o "[a-zA-Z]\+-[a-zA-Z]\+-[0-9a-z]\+\>") |
|
| 43 | - aws ec2 describe-subnets | jq -r '.Subnets[] | select( .AvailabilityZone =="'"$MY_AZ"'") | .CidrBlock | split("/") | .[1]' > $SUBNET_MASK_SIZE_VAR_LOCATION |
|
| 44 | -fi |
|
| 45 | -if [[ "$PRODUCTION_IP" == "\${${ARCHIVE_IP_NAME}}" ]] |
|
| 46 | -then |
|
| 47 | - # Check if main archive is in the same az by comparing the network portion of the archive and this instance's IP, with the same bitmask. |
|
| 48 | - MY_IP=$( ec2-metadata --local-ipv4 | grep -o "[0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+\>") |
|
| 49 | - MY_IP_VALUE="$(convert_ip_to_num "$MY_IP")" |
|
| 50 | - MY_AZ_MASK_SIZE=$(cat $SUBNET_MASK_SIZE_VAR_LOCATION) |
|
| 51 | - # convert the value after the CIDR slash into a bitmask |
|
| 52 | - BITMASK_VALUE=0 |
|
| 53 | - for i in $(seq $((31 - $MY_AZ_MASK_SIZE + 1)) 31); do |
|
| 54 | - BITMASK_VALUE=$(($BITMASK_VALUE | (1 << i ) )) |
|
| 55 | - done |
|
| 56 | - MY_SUBNET_VALUE=$(($BITMASK_VALUE & $MY_IP_VALUE)) |
|
| 57 | - # Extract the actual IP of the archive |
|
| 58 | - ARCHIVE_IP=$(grep -m 1 "^Define ${ARCHIVE_IP_NAME}\> .*" ${MACROS_PATH} | grep -o "${IP_REGEX}") |
|
| 59 | - ARCHIVE_IP_VALUE=$(convert_ip_to_num "$ARCHIVE_IP") |
|
| 60 | - ARCHIVE_SUBNET_VALUE=$(($BITMASK_VALUE & $ARCHIVE_IP_VALUE )) #A Aply the mask to the archive IP value. |
|
| 61 | - if [[ "$ARCHIVE_SUBNET_VALUE" -eq "$MY_SUBNET_VALUE" ]]; then |
|
| 62 | - status "200" |
|
| 63 | - outputMessage "Healthy: in the same az as the archive" |
|
| 64 | - else |
|
| 65 | - status "503 Not in the same az" |
|
| 66 | - outputMessage "Unhealthy: Not in the same az as the archive" |
|
| 67 | - fi |
|
| 68 | -else |
|
| 69 | - # We don't check if failover archive is in the same az |
|
| 70 | - status "200" |
|
| 71 | - outputMessage "failover at play: force healthy" |
|
| 72 | -fi |
|
| 25 | +fi |
|
| ... | ... | \ No newline at end of file |
configuration/environments_scripts/reverse_proxy/setup-disposable-reverse-proxy.sh
| ... | ... | @@ -0,0 +1,64 @@ |
| 1 | +#!/bin/bash |
|
| 2 | + |
|
| 3 | +# Setup script for Amazon Linux 2. May need to update macro definitions for the archive IP. |
|
| 4 | +# Parameter 1 is the IP and parameter 2 is the bearer token to be installed in the root home dir. |
|
| 5 | +# Ensure that the security for requesting the metadata uses IMDSv1 |
|
| 6 | +IP=$1 |
|
| 7 | +BEARER_TOKEN=$2 |
|
| 8 | +IMAGE_TYPE="reverse_proxy" |
|
| 9 | +HTTP_LOGROTATE_ABSOLUTE=/etc/logrotate.d/httpd |
|
| 10 | +GIT_COPY_USER="trac" |
|
| 11 | +RELATIVE_PATH_TO_GIT="gitcopy" # the relative path to the repo within the git_copy_user |
|
| 12 | +ssh -A "ec2-user@${IP}" "bash -s" << FIRSTEOF |
|
| 13 | +# Correct authorized keys. May not be necessary if update_authorized_keys is running. |
|
| 14 | +sudo su - -c "cat ~ec2-user/.ssh/authorized_keys > /root/.ssh/authorized_keys" |
|
| 15 | +FIRSTEOF |
|
| 16 | +# writes std error to local text file |
|
| 17 | +ssh -A "root@${IP}" "bash -s" << SECONDEOF >stdoutLog.txt |
|
| 18 | +# fstab setup |
|
| 19 | +mkdir /var/log/old |
|
| 20 | +echo "logfiles.internal.sapsailing.com:/var/log/old /var/log/old nfs tcp,intr,timeo=100,retry=0" >> /etc/fstab |
|
| 21 | +mount -a |
|
| 22 | +# update instance |
|
| 23 | +yum update -y |
|
| 24 | +yum install -y httpd mod_proxy_html tmux nfs-utils git whois jq cronie iptables mod_ssl |
|
| 25 | +sudo systemctl enable crond.service |
|
| 26 | +# setup other users and crontabs to keep repo updated |
|
| 27 | +cd /root |
|
| 28 | +scp -o StrictHostKeyChecking=no -p "root@sapsailing.com:/home/wiki/gitwiki/configuration/environments_scripts/repo/usr/local/bin/imageupgrade_functions.sh" /usr/local/bin |
|
| 29 | +# Setup root user and apache user with the right keys. |
|
| 30 | +. imageupgrade_functions.sh |
|
| 31 | +setup_keys "${IMAGE_TYPE}" |
|
| 32 | +setup_cloud_cfg_and_root_login |
|
| 33 | +# setup symbolic links and crontab |
|
| 34 | +build_crontab_and_setup_files "${IMAGE_TYPE}" "${GIT_COPY_USER}" "${RELATIVE_PATH_TO_GIT}" |
|
| 35 | +# setup mail |
|
| 36 | +setup_mail_sending |
|
| 37 | +cd /usr/local/bin |
|
| 38 | +echo $BEARER_TOKEN > /root/ssh-key-reader.token |
|
| 39 | +# add basic test page which won't cause redirect error code if used as a health check. |
|
| 40 | +cat <<EOF > /var/www/html/index.html |
|
| 41 | +<!DOCTYPE html><html lang="en"><head><title>Health check</title><meta charset="UTF-8"></head><body><h1>Test page</h1></body></html> |
|
| 42 | +EOF |
|
| 43 | +# ensure httpd starts on startup |
|
| 44 | +systemctl enable httpd |
|
| 45 | +echo "net.ipv4.ip_conntrac_max = 131072" >> /etc/sysctl.conf |
|
| 46 | +# setup fail2ban |
|
| 47 | +setup_fail2ban |
|
| 48 | +# mount nvme if available |
|
| 49 | +mountnvmeswap |
|
| 50 | +# setup logrotate.d/httpd |
|
| 51 | +mkdir /var/log/logrotate-target |
|
| 52 | +echo "Patching $HTTP_LOGROTATE_ABSOLUTE so that old logs go to /var/log/old/$IP" >>/var/log/sailing.out |
|
| 53 | +mkdir --parents "/var/log/old/REVERSE_PROXIES/${IP}" |
|
| 54 | +sed -i "s|/var/log/old|/var/log/old/REVERSE_PROXIES/${IP}|" $HTTP_LOGROTATE_ABSOLUTE |
|
| 55 | +# logrotate.conf setup |
|
| 56 | +sed -i 's/rotate 4/rotate 20 \n\nolddir \/var\/log\/logrotate-target/' /etc/logrotate.conf |
|
| 57 | +sed -i "s/^#compress/compress/" /etc/logrotate.conf |
|
| 58 | +# setup git |
|
| 59 | +/root/setupHttpdGitLocal.sh "httpdConf@sapsailing.com:repo.git" |
|
| 60 | +# Final enabling and starting of services. |
|
| 61 | +systemctl start httpd |
|
| 62 | +sudo systemctl start crond.service |
|
| 63 | +systemctl enable imageupgrade.service |
|
| 64 | +SECONDEOF |
configuration/httpd/conf.d/000-macros.conf
| ... | ... | @@ -64,6 +64,10 @@ Define PRODUCTION_ARCHIVE ${ARCHIVE_IP} |
| 64 | 64 | Order deny,allow |
| 65 | 65 | Allow from all |
| 66 | 66 | </Location> |
| 67 | + <Location "/cgi-bin"> |
|
| 68 | + # Allow cgi-bin access to permit custom status scripts too. |
|
| 69 | + Allow from all |
|
| 70 | + </Location> |
|
| 67 | 71 | CustomLog logs/services_log combined |
| 68 | 72 | </VirtualHost> |
| 69 | 73 | </Macro> |
configuration/httpd/conf.d/000-main.conf
| ... | ... | @@ -8,6 +8,7 @@ |
| 8 | 8 | # New approach: a single RewriteRule that catches all HTTP requests, mapping them to corresponding HTTPS requests |
| 9 | 9 | RewriteEngine on |
| 10 | 10 | RewriteRule ^.*$ https://www.sapsailing.com%{REQUEST_URI} [L,R=302,NE] |
| 11 | + RewriteCond %{HTTP_USER_AGENT} !ELB-HealthChecker/2.0 |
|
| 11 | 12 | CustomLog logs/redirect_log sapsailing_httpd_redirect_combined env=!original_client_ip |
| 12 | 13 | CustomLog logs/redirect_log sapsailing_httpd_redirect_combined_first_forwarded_for_ip env=original_client_ip |
| 13 | 14 | </VirtualHost> |
| ... | ... | @@ -23,7 +24,7 @@ |
| 23 | 24 | RewriteCond %{REQUEST_METHOD} GET |
| 24 | 25 | RewriteCond %{REQUEST_URI} /.well-known/assetlinks.json |
| 25 | 26 | RewriteRule ^.*$ http://${ARCHIVE_IP}:8888%{REQUEST_URI} [P] |
| 26 | - |
|
| 27 | + RewriteCond %{HTTP_USER_AGENT} !ELB-HealthChecker/2.0 |
|
| 27 | 28 | RewriteCond %{HTTP:X-Forwarded-Proto} !https |
| 28 | 29 | RewriteCond %{HTTPS} off |
| 29 | 30 | RewriteRule ^.*$ https://www.sapsailing.com%{REQUEST_URI} [L,R=301,NE] |
configuration/httpd/conf/httpd.conf
| ... | ... | @@ -148,7 +148,7 @@ Include conf.modules.d/*.conf |
| 148 | 148 | |
| 149 | 149 | LoadModule usertrack_module modules/mod_usertrack.so |
| 150 | 150 | LoadModule speling_module modules/mod_speling.so |
| 151 | -LoadModule php7_module modules/libphp-7.1.so |
|
| 151 | +#LoadModule php7_module modules/libphp-7.1.so |
|
| 152 | 152 | |
| 153 | 153 | |
| 154 | 154 | <IfModule mod_php7.c> |
configuration/mongo_instance_setup/crontab
| ... | ... | @@ -1,3 +1,3 @@ |
| 1 | 1 | * * * * * export PATH=/bin:/usr/bin:/usr/local/bin; sleep $(( $RANDOM * 60 / 32768 )); update_authorized_keys_for_landscape_managers_if_changed $( cat /root/ssh-key-reader.token ) https://security-service.sapsailing.com /home/ec2-user 2>&1 >>/var/log/sailing.err |
| 2 | 2 | # NOTICE: Please try to reference the customised crontabs at $GIT_HOME/configuration/crontabs or use |
| 3 | -# the build-crontab script. This file has been maintained for continuity, but is deprecated. |
|
| ... | ... | \ No newline at end of file |
| 0 | +# the build-crontab-and-cp-files script. This file has been maintained for continuity, but is deprecated. |
|
| ... | ... | \ No newline at end of file |
configuration/mysql_instance_setup/crontab-ec2-user
| ... | ... | @@ -1,3 +1,3 @@ |
| 1 | 1 | * * * * * export PATH=/bin:/usr/bin:/usr/local/bin; sleep $(( $RANDOM * 60 / 32768 )); update_authorized_keys_for_landscape_managers_if_changed $( cat /home/ec2-user/ssh-key-reader.token ) https://security-service.sapsailing.com /home/ec2-user |
| 2 | 2 | # NOTICE: Please try to reference the customised crontabs at $GIT_HOME/configuration/crontabs or use |
| 3 | -# the build-crontab script. This file has been maintained for continuity, but is deprecated. |
|
| 3 | +# the build-crontab-and-cp-files script. This file has been maintained for continuity, but is deprecated. |
configuration/sailing_server_setup/crontab-root
| ... | ... | @@ -1,3 +1,3 @@ |
| 1 | 1 | * * * * * export PATH=/bin:/usr/bin:/usr/local/bin; sleep $(( $RANDOM * 60 / 32768 )); update_authorized_keys_for_landscape_managers_if_changed $( cat /root/ssh-key-reader.token ) https://security-service.sapsailing.com /root 2>&1 >>/var/log/sailing.err |
| 2 | 2 | # NOTICE: Please try to reference the customised crontabs at $GIT_HOME/configuration/crontabs or use |
| 3 | -# the build-crontab script. This file has been maintained for continuity, but is deprecated. |
|
| 3 | +# the build-crontab-and-cp-files script. This file has been maintained for continuity, but is deprecated. |
doc/b1873 - Disposable reverse proxy architecture.pptx
| ... | ... | Binary files /dev/null and b/doc/b1873 - Disposable reverse proxy architecture.pptx differ |
java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/Competitor.java
| ... | ... | @@ -97,4 +97,14 @@ public interface Competitor extends NamedWithID, IsManagedByCache<SharedDomainFa |
| 97 | 97 | */ |
| 98 | 98 | Duration getTimeOnDistanceAllowancePerNauticalMile(); |
| 99 | 99 | |
| 100 | + /** |
|
| 101 | + * When two competitors score the same in a leaderboard and the tie cannot be broken, in order to avoid oscillations |
|
| 102 | + * when users roam across different replicas, a stable ordering criterion is still required. This may be the name |
|
| 103 | + * or the sail number (in case this is a competitor with boat).<p> |
|
| 104 | + * |
|
| 105 | + * This default implementation uses the {@link #getName() name}. |
|
| 106 | + */ |
|
| 107 | + default String getStableLastResortOrderingCriterion() { |
|
| 108 | + return getName(); |
|
| 109 | + } |
|
| 100 | 110 | } |
java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/CompetitorWithBoat.java
| ... | ... | @@ -6,4 +6,8 @@ package com.sap.sailing.domain.base; |
| 6 | 6 | * @author fmittag |
| 7 | 7 | */ |
| 8 | 8 | public interface CompetitorWithBoat extends Competitor, WithBoat { |
| 9 | + @Override |
|
| 10 | + default String getStableLastResortOrderingCriterion() { |
|
| 11 | + return getBoat().getSailID(); |
|
| 12 | + } |
|
| 9 | 13 | } |
java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/LeaderboardTotalRankComparator.java
| ... | ... | @@ -357,7 +357,7 @@ public class LeaderboardTotalRankComparator implements Comparator<Competitor> { |
| 357 | 357 | } |
| 358 | 358 | |
| 359 | 359 | private int compareByArbitraryButStableCriteria(Competitor o1, Competitor o2) { |
| 360 | - return o1.getName().compareTo(o2.getName()); |
|
| 360 | + return o1.getStableLastResortOrderingCriterion().compareTo(o2.getStableLastResortOrderingCriterion()); |
|
| 361 | 361 | } |
| 362 | 362 | |
| 363 | 363 | private static class FleetComparisonResult { |
java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/whatsnew/resources/SailingAnalyticsNotes.html
| ... | ... | @@ -14,6 +14,10 @@ |
| 14 | 14 | This may have led to inconsistent displays.</li> |
| 15 | 15 | <li>Bug fix: the new drop-down race choose in the Race Board failed to work if the race from which it |
| 16 | 16 | was used does not have a valid start time yet.</li> |
| 17 | + <li>In case of an unbreakable tie, instead of always using the competitor name to arrive at some stable |
|
| 18 | + sorting, now we use the sail number in case boats cannot change during the regatta (as they could in, |
|
| 19 | + say, a league format). This makes sorting more consistent with regatta management systems such |
|
| 20 | + as Manage2Sail.</li> |
|
| 17 | 21 | </ul> |
| 18 | 22 | <h5 class="articleSubheadline">February 2024</h5> |
| 19 | 23 | <ul class="bulletList"> |
java/com.sap.sailing.ingestion/pom.xml
| ... | ... | @@ -47,7 +47,7 @@ |
| 47 | 47 | <dependency> |
| 48 | 48 | <groupId>software.amazon.awssdk</groupId> |
| 49 | 49 | <artifactId>bom</artifactId> |
| 50 | - <version>2.18.8</version> |
|
| 50 | + <version>2.20.59</version> |
|
| 51 | 51 | <type>pom</type> |
| 52 | 52 | <scope>import</scope> |
| 53 | 53 | </dependency> |
java/com.sap.sailing.ingestion/src/main/java/com/sap/sailing/ingestion/AWSInOutHandler.java
| ... | ... | @@ -23,7 +23,8 @@ public class AWSInOutHandler { |
| 23 | 23 | public JSONObject parseInputToJson(InputStream inputAsStream) |
| 24 | 24 | throws IOException, JsonDeserializationException, ParseException { |
| 25 | 25 | final byte[] streamAsBytes = IoUtils.toByteArray(inputAsStream); |
| 26 | - final Object awsRequestObject = JSONValue.parseWithException(new String(streamAsBytes)); |
|
| 26 | + final String s = new String(streamAsBytes); |
|
| 27 | + final Object awsRequestObject = JSONValue.parseWithException(s); |
|
| 27 | 28 | final JSONObject awsRequestJson = Helpers.toJSONObjectSafe(awsRequestObject); |
| 28 | 29 | final AWSRequestWrapper requestWrapped = awsRequestDeserializer.deserialize(awsRequestJson); |
| 29 | 30 | final Object requestBody = JSONValue.parseWithException(requestWrapped.getBody()); |
java/com.sap.sailing.ingestion/src/main/java/com/sap/sailing/ingestion/Configuration.java
| ... | ... | @@ -7,7 +7,7 @@ public interface Configuration { |
| 7 | 7 | * This host is only reachable through VPC and therefore one needs to ensure that the Lambda has all relevant |
| 8 | 8 | * permissions |
| 9 | 9 | */ |
| 10 | - String[] REDIS_ENDPOINTS = { "redis://fixingestionrediscache.cvoblp.ng.0001.euw2.cache.amazonaws.com:6379" }; |
|
| 10 | + String[] REDIS_ENDPOINTS = { "redis://fixingestionrediscache-serverless-cvoblp.serverless.euw2.cache.amazonaws.com:6379" }; |
|
| 11 | 11 | String REDIS_MAP_NAME = "endpoints"; |
| 12 | 12 | |
| 13 | 13 | Region S3_REGION = Region.EU_WEST_2; |
java/com.sap.sailing.landscape.common/src/com/sap/sailing/landscape/common/SharedLandscapeConstants.java
| ... | ... | @@ -90,4 +90,9 @@ public interface SharedLandscapeConstants { |
| 90 | 90 | * of a typical process instance. |
| 91 | 91 | */ |
| 92 | 92 | int DEFAULT_NUMBER_OF_PROCESSES_IN_MEMORY = 4; |
| 93 | + |
|
| 94 | + /** |
|
| 95 | + * The most appropriate instance type for a disposable reverse proxy. |
|
| 96 | + */ |
|
| 97 | + String DEFAULT_REVERSE_PROXY_INSTANCE_TYPE = "T3_MEDIUM"; |
|
| 93 | 98 | } |
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/CreateReverseProxyInClusterDialog.java
| ... | ... | @@ -0,0 +1,199 @@ |
| 1 | +package com.sap.sailing.landscape.ui.client; |
|
| 2 | + |
|
| 3 | +import java.util.ArrayList; |
|
| 4 | +import java.util.List; |
|
| 5 | +import java.util.Map; |
|
| 6 | +import java.util.stream.Collectors; |
|
| 7 | + |
|
| 8 | +import com.google.gwt.user.client.rpc.IsSerializable; |
|
| 9 | +import com.google.gwt.user.client.ui.FocusWidget; |
|
| 10 | +import com.google.gwt.user.client.ui.FormPanel; |
|
| 11 | +import com.google.gwt.user.client.ui.Label; |
|
| 12 | +import com.google.gwt.user.client.ui.ListBox; |
|
| 13 | +import com.google.gwt.user.client.ui.TextBox; |
|
| 14 | +import com.google.gwt.user.client.ui.VerticalPanel; |
|
| 15 | +import com.google.gwt.user.client.ui.Widget; |
|
| 16 | +import com.sap.sailing.landscape.common.SharedLandscapeConstants; |
|
| 17 | +import com.sap.sailing.landscape.ui.client.i18n.StringMessages; |
|
| 18 | +import com.sap.sailing.landscape.ui.shared.AvailabilityZoneDTO; |
|
| 19 | +import com.sap.sailing.landscape.ui.shared.ReverseProxyDTO; |
|
| 20 | +import com.sap.sse.common.Util; |
|
| 21 | +import com.sap.sse.gwt.client.ErrorReporter; |
|
| 22 | +import com.sap.sse.gwt.client.dialog.DataEntryDialog; |
|
| 23 | + |
|
| 24 | +/** |
|
| 25 | + * Creates a dialog box for adding a reverse proxy to the cluster. |
|
| 26 | + * |
|
| 27 | + * @author Thomas Stokes |
|
| 28 | + * |
|
| 29 | + */ |
|
| 30 | +public class CreateReverseProxyInClusterDialog |
|
| 31 | + extends DataEntryDialog<CreateReverseProxyInClusterDialog.CreateReverseProxyInstructions> { |
|
| 32 | + |
|
| 33 | + public static class CreateReverseProxyInstructions implements IsSerializable { |
|
| 34 | + private final String instanceType; |
|
| 35 | + private final String name; |
|
| 36 | + private final String region; |
|
| 37 | + private String keyName; |
|
| 38 | + private final AvailabilityZoneDTO availabilityZoneDTO; |
|
| 39 | + |
|
| 40 | + /** |
|
| 41 | + * @param name |
|
| 42 | + * The name of the reverse proxy to spawn. |
|
| 43 | + * @param instanceType |
|
| 44 | + * The new instance type. |
|
| 45 | + * @param availabilityZoneNameListBox |
|
| 46 | + * The id of the AZ with the fewest reverse proxies. |
|
| 47 | + * @param region |
|
| 48 | + */ |
|
| 49 | + public CreateReverseProxyInstructions(String name, String instanceType, |
|
| 50 | + String region, AvailabilityZoneDTO availabilityZoneDTO) { |
|
| 51 | + this.name = name; |
|
| 52 | + this.instanceType = instanceType; |
|
| 53 | + this.region = region; |
|
| 54 | + this.availabilityZoneDTO = availabilityZoneDTO; |
|
| 55 | + } |
|
| 56 | + |
|
| 57 | + public String getName() { |
|
| 58 | + return name; |
|
| 59 | + } |
|
| 60 | + |
|
| 61 | + public String getInstanceType() { |
|
| 62 | + return instanceType; |
|
| 63 | + } |
|
| 64 | + |
|
| 65 | + public String getRegion() { |
|
| 66 | + return region; |
|
| 67 | + } |
|
| 68 | + |
|
| 69 | + public String getKey() { |
|
| 70 | + return keyName; |
|
| 71 | + } |
|
| 72 | + |
|
| 73 | + public void setKey(String key) { |
|
| 74 | + keyName = key; |
|
| 75 | + } |
|
| 76 | + |
|
| 77 | + public AvailabilityZoneDTO getAvailabilityZoneDTO() { |
|
| 78 | + return availabilityZoneDTO; |
|
| 79 | + } |
|
| 80 | + } |
|
| 81 | + |
|
| 82 | + private final TextBox proxyName; |
|
| 83 | + private final ListBox dedicatedInstanceTypeListBox; |
|
| 84 | + private final ListBox availabilityZoneNameListBox; |
|
| 85 | + private final Map<String, String> availabilityZoneNameToId; |
|
| 86 | + private final String region; |
|
| 87 | + private final Label nameLabel; |
|
| 88 | + private final Label instanceTypeLabel; |
|
| 89 | + private final Label availabilityZoneLabel; |
|
| 90 | + private Label instancesIdLabel; |
|
| 91 | + |
|
| 92 | + /** |
|
| 93 | + * A list of all labels in the dialog box. |
|
| 94 | + */ |
|
| 95 | + private ArrayList<Label> labels; |
|
| 96 | + |
|
| 97 | + /** |
|
| 98 | + * The dialog box allows users to choose the name, instance type and az. |
|
| 99 | + * |
|
| 100 | + * @param existingReverseProxies A list of reverse proxy DTOs that exist in the region. |
|
| 101 | + * @param availabilityZones A list of the availability zone (in DTO format) in the region. |
|
| 102 | + * @param callback |
|
| 103 | + * This will call after the user selects the "ok" in the box, if they create a reverse proxy. |
|
| 104 | + */ |
|
| 105 | + public CreateReverseProxyInClusterDialog(StringMessages stringMessages, ErrorReporter errorReporter, |
|
| 106 | + LandscapeManagementWriteServiceAsync landscapeManagementService, String region, |
|
| 107 | + List<ReverseProxyDTO> existingReverseProxies, List<AvailabilityZoneDTO> availabilityZones, |
|
| 108 | + DialogCallback<CreateReverseProxyInClusterDialog.CreateReverseProxyInstructions> callback) { |
|
| 109 | + super(stringMessages.reverseProxies(), stringMessages.reverseProxies(), stringMessages.ok(), |
|
| 110 | + stringMessages.cancel(), |
|
| 111 | + new Validator<CreateReverseProxyInClusterDialog.CreateReverseProxyInstructions>() { |
|
| 112 | + |
|
| 113 | + @Override |
|
| 114 | + public String getErrorMessage( |
|
| 115 | + CreateReverseProxyInClusterDialog.CreateReverseProxyInstructions valueToValidate) { |
|
| 116 | + if (!Util.hasLength(valueToValidate.getName())) { |
|
| 117 | + return stringMessages.pleaseProvideNonEmptyNameAndAZ(); |
|
| 118 | + } else if (valueToValidate.availabilityZoneDTO == null) { |
|
| 119 | + return stringMessages.pleaseProvideNonEmptyNameAndAZ(); |
|
| 120 | + } else { |
|
| 121 | + return null; |
|
| 122 | + } |
|
| 123 | + } |
|
| 124 | + }, callback); |
|
| 125 | + this.region = region; |
|
| 126 | + availabilityZoneNameToId = availabilityZones.stream().collect(Collectors.toMap(entry -> entry.getAzName(), entry -> entry.getAzId())); |
|
| 127 | + availabilityZoneNameListBox = setupAZChoiceListBox(landscapeManagementService, errorReporter, |
|
| 128 | + existingReverseProxies); |
|
| 129 | + proxyName = createTextBox("", 20); |
|
| 130 | + dedicatedInstanceTypeListBox = LandscapeDialogUtil.createInstanceTypeListBox(this, landscapeManagementService, |
|
| 131 | + stringMessages, SharedLandscapeConstants.DEFAULT_REVERSE_PROXY_INSTANCE_TYPE, errorReporter); |
|
| 132 | + // setup labels |
|
| 133 | + nameLabel = new Label(stringMessages.instanceName()); |
|
| 134 | + instanceTypeLabel = new Label(stringMessages.instanceType()); |
|
| 135 | + availabilityZoneLabel = new Label(stringMessages.availabilityZone()); |
|
| 136 | + instancesIdLabel = new Label(stringMessages.instanceId()); |
|
| 137 | + labels = new ArrayList<>(4); |
|
| 138 | + labels.add(nameLabel); |
|
| 139 | + labels.add(instanceTypeLabel); |
|
| 140 | + labels.add(availabilityZoneLabel); |
|
| 141 | + labels.add(instancesIdLabel); |
|
| 142 | + instancesIdLabel.setVisible(false); |
|
| 143 | + validateAndUpdate(); |
|
| 144 | + } |
|
| 145 | + |
|
| 146 | + /** |
|
| 147 | + * |
|
| 148 | + * This method assumes that all AZs are being served. If an instance is deployed in an AZ not served by a target group, it will be "unused". |
|
| 149 | + * @param existingReverseProxies A list of all existing reverse proxies in a region. |
|
| 150 | + * @return A listbox with the reverse proxies. The least populated az is selected by default. |
|
| 151 | + */ |
|
| 152 | + private ListBox setupAZChoiceListBox(LandscapeManagementWriteServiceAsync landscapeManagementService, |
|
| 153 | + ErrorReporter errorReporter, List<ReverseProxyDTO> existingReverseProxies) { |
|
| 154 | + final ListBox availabilityZoneBox = createListBox(false); |
|
| 155 | + if (!availabilityZoneNameToId.isEmpty()) { |
|
| 156 | + final Map<String, Long> azCounts = existingReverseProxies.stream() // Maps the AZs name to the number of times a reverse proxy is in that AZ. |
|
| 157 | + .collect(Collectors.groupingBy(w -> w.getAvailabilityZoneName(), Collectors.counting())); |
|
| 158 | + availabilityZoneNameToId.keySet().forEach(azName -> azCounts.merge(azName, 0L, (a, b) -> a + b)); // Merges in any AZ which has no reverse proxies. |
|
| 159 | + final String leastPopulatedAzName = azCounts.entrySet().stream() |
|
| 160 | + .min((a, b) -> Long.compare(a.getValue(), b.getValue())).get().getKey(); |
|
| 161 | + int i = 0; |
|
| 162 | + for (String az : availabilityZoneNameToId.keySet().stream().sorted().collect(Collectors.toList())) { |
|
| 163 | + availabilityZoneBox.addItem(az, az); |
|
| 164 | + if (az.equals(leastPopulatedAzName)) { |
|
| 165 | + availabilityZoneBox.setSelectedIndex(i); |
|
| 166 | + } |
|
| 167 | + i++; |
|
| 168 | + } |
|
| 169 | + } |
|
| 170 | + return availabilityZoneBox; |
|
| 171 | + } |
|
| 172 | + |
|
| 173 | + @Override |
|
| 174 | + protected Widget getAdditionalWidget() { |
|
| 175 | + final FormPanel result = new FormPanel(); |
|
| 176 | + final VerticalPanel verticalPanel = new VerticalPanel(); |
|
| 177 | + result.add(verticalPanel); |
|
| 178 | + verticalPanel.add(nameLabel); |
|
| 179 | + verticalPanel.add(proxyName); |
|
| 180 | + verticalPanel.add(instanceTypeLabel); |
|
| 181 | + verticalPanel.add(dedicatedInstanceTypeListBox); |
|
| 182 | + verticalPanel.add(availabilityZoneLabel); |
|
| 183 | + verticalPanel.add(availabilityZoneNameListBox); |
|
| 184 | + return result; |
|
| 185 | + } |
|
| 186 | + |
|
| 187 | + @Override |
|
| 188 | + protected FocusWidget getInitialFocusWidget() { |
|
| 189 | + return proxyName; |
|
| 190 | + } |
|
| 191 | + |
|
| 192 | + @Override |
|
| 193 | + protected CreateReverseProxyInClusterDialog.CreateReverseProxyInstructions getResult() { |
|
| 194 | + return new CreateReverseProxyInstructions(proxyName.getValue(), dedicatedInstanceTypeListBox.getSelectedValue(), |
|
| 195 | + region, |
|
| 196 | + new AvailabilityZoneDTO(availabilityZoneNameListBox.getSelectedValue(), region, |
|
| 197 | + availabilityZoneNameToId.get(availabilityZoneNameListBox.getSelectedValue()))); |
|
| 198 | + } |
|
| 199 | +} |
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/LandscapeDialogUtil.java
| ... | ... | @@ -14,9 +14,9 @@ public class LandscapeDialogUtil { |
| 14 | 14 | public static ListBox createInstanceTypeListBox(DataEntryDialog<?> dialog, |
| 15 | 15 | LandscapeManagementWriteServiceAsync landscapeManagementService, StringMessages stringMessages, |
| 16 | 16 | String defaultInstanceTypeName, ErrorReporter errorReporter) { |
| 17 | - return createInstanceTypeListBoxWithAdditionalDefaultEntry(dialog, |
|
| 18 | - /* additionalItem */ null, /* additionalValue */ null, |
|
| 19 | - landscapeManagementService, stringMessages, defaultInstanceTypeName, errorReporter); |
|
| 17 | + return createInstanceTypeListBoxWithAdditionalDefaultEntry(dialog, /* additionalItem */ null, |
|
| 18 | + /* additionalValue */ null, landscapeManagementService, stringMessages, defaultInstanceTypeName, |
|
| 19 | + errorReporter); |
|
| 20 | 20 | } |
| 21 | 21 | |
| 22 | 22 | /** |
| ... | ... | @@ -29,7 +29,7 @@ public class LandscapeDialogUtil { |
| 29 | 29 | String additionalItem, String additionalValue, |
| 30 | 30 | LandscapeManagementWriteServiceAsync landscapeManagementService, StringMessages stringMessages, |
| 31 | 31 | String defaultInstanceTypeName, ErrorReporter errorReporter) { |
| 32 | - final ListBox instanceTypeBox = dialog.createListBox(/*isMultipleSelect*/false); |
|
| 32 | + final ListBox instanceTypeBox = dialog.createListBox(/* isMultipleSelect */false); |
|
| 33 | 33 | if (additionalItem != null) { |
| 34 | 34 | instanceTypeBox.addItem(additionalItem, additionalValue); |
| 35 | 35 | instanceTypeBox.setSelectedIndex(0); |
| ... | ... | @@ -39,11 +39,11 @@ public class LandscapeDialogUtil { |
| 39 | 39 | public void onFailure(Throwable caught) { |
| 40 | 40 | errorReporter.reportError(caught.getMessage()); |
| 41 | 41 | } |
| 42 | - |
|
| 42 | + |
|
| 43 | 43 | @Override |
| 44 | 44 | public void onSuccess(ArrayList<String> result) { |
| 45 | 45 | Collections.sort(result, new NaturalComparator()); |
| 46 | - int i=0; |
|
| 46 | + int i = 0; |
|
| 47 | 47 | for (final String instanceType : result) { |
| 48 | 48 | instanceTypeBox.addItem(instanceType, instanceType); |
| 49 | 49 | if (additionalItem == null && instanceType.equals(defaultInstanceTypeName)) { |
| ... | ... | @@ -57,7 +57,7 @@ public class LandscapeDialogUtil { |
| 57 | 57 | } |
| 58 | 58 | |
| 59 | 59 | public static void selectInstanceType(ListBox instanceTypeListBox, String instanceTypeName) { |
| 60 | - for (int i=0; i<instanceTypeListBox.getItemCount(); i++) { |
|
| 60 | + for (int i = 0; i < instanceTypeListBox.getItemCount(); i++) { |
|
| 61 | 61 | if (instanceTypeListBox.getValue(i).equals(instanceTypeName)) { |
| 62 | 62 | instanceTypeListBox.setSelectedIndex(i); |
| 63 | 63 | break; |
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/LandscapeManagementPanel.java
| ... | ... | @@ -41,12 +41,14 @@ import com.sap.sailing.landscape.ui.client.SwitchToReplicaOnSharedInstanceDialog |
| 41 | 41 | import com.sap.sailing.landscape.ui.client.UpgradeApplicationReplicaSetDialog.UpgradeApplicationReplicaSetInstructions; |
| 42 | 42 | import com.sap.sailing.landscape.ui.client.i18n.StringMessages; |
| 43 | 43 | import com.sap.sailing.landscape.ui.shared.AmazonMachineImageDTO; |
| 44 | +import com.sap.sailing.landscape.ui.shared.AvailabilityZoneDTO; |
|
| 44 | 45 | import com.sap.sailing.landscape.ui.shared.AwsInstanceDTO; |
| 45 | 46 | import com.sap.sailing.landscape.ui.shared.CompareServersResultDTO; |
| 46 | 47 | import com.sap.sailing.landscape.ui.shared.MongoEndpointDTO; |
| 47 | 48 | import com.sap.sailing.landscape.ui.shared.MongoScalingInstructionsDTO; |
| 48 | 49 | import com.sap.sailing.landscape.ui.shared.ProcessDTO; |
| 49 | 50 | import com.sap.sailing.landscape.ui.shared.ReleaseDTO; |
| 51 | +import com.sap.sailing.landscape.ui.shared.ReverseProxyDTO; |
|
| 50 | 52 | import com.sap.sailing.landscape.ui.shared.SSHKeyPairDTO; |
| 51 | 53 | import com.sap.sailing.landscape.ui.shared.SailingAnalyticsProcessDTO; |
| 52 | 54 | import com.sap.sailing.landscape.ui.shared.SailingApplicationReplicaSetDTO; |
| ... | ... | @@ -114,9 +116,10 @@ public class LandscapeManagementPanel extends SimplePanel { |
| 114 | 116 | private final SimpleBusyIndicator applicationReplicaSetsBusy; |
| 115 | 117 | private final ErrorReporter errorReporter; |
| 116 | 118 | private final AwsMfaLoginWidget mfaLoginWidget; |
| 119 | + private TableWrapperWithMultiSelectionAndFilter<ReverseProxyDTO, StringMessages, AdminConsoleTableResources> proxiesTable; |
|
| 120 | + private final BusyIndicator proxiesTableBusy; |
|
| 117 | 121 | private final static String AWS_DEFAULT_REGION_USER_PREFERENCE = "aws.region.default"; |
| 118 | 122 | private final static Duration DURATION_TO_WAIT_BETWEEN_REPLICA_SET_UPGRADE_REQUESTS = Duration.ONE_MINUTE; |
| 119 | - |
|
| 120 | 123 | /** |
| 121 | 124 | * The time to wait after archiving a replica set and before starting a "compare servers" run for the content |
| 122 | 125 | * archived. This waiting period is owed to the process of loading the race content which is an asynchronous |
| ... | ... | @@ -465,46 +468,137 @@ public class LandscapeManagementPanel extends SimplePanel { |
| 465 | 468 | final CaptionPanel machineImagesCaptionPanel = new CaptionPanel(stringMessages.machineImages()); |
| 466 | 469 | final VerticalPanel machineImagesVerticalPanel = new VerticalPanel(); |
| 467 | 470 | machineImagesCaptionPanel.add(machineImagesVerticalPanel); |
| 471 | + final Button machineTableRefreshButton = new Button(stringMessages.refresh()); |
|
| 472 | + machineImagesVerticalPanel.add(machineTableRefreshButton); |
|
| 473 | + machineTableRefreshButton.addClickHandler(event -> refreshMachineImagesTable()); |
|
| 468 | 474 | machineImagesVerticalPanel.add(machineImagesTable); |
| 469 | 475 | machineImagesBusy = new SimpleBusyIndicator(); |
| 470 | 476 | machineImagesVerticalPanel.add(machineImagesBusy); |
| 471 | 477 | mainPanel.add(machineImagesCaptionPanel); |
| 472 | - regionsTable.getSelectionModel().addSelectionChangeHandler(e-> |
|
| 473 | - { |
|
| 474 | - final String selectedRegion = regionsTable.getSelectionModel().getSelectedObject(); |
|
| 475 | - refreshAllThatNeedsAwsCredentials(); |
|
| 476 | - storeRegionSelection(userService, selectedRegion); |
|
| 477 | - }); |
|
| 478 | - AsyncCallback<Boolean> validatePassphraseCallback = new AsyncCallback<Boolean>() { |
|
| 479 | - @Override |
|
| 480 | - public void onSuccess(Boolean result) { |
|
| 481 | - sshKeyManagementPanel.setPassphraseValidation(result.booleanValue(), stringMessages); |
|
| 482 | - addApplicationReplicaSetButton.setVisible(result); |
|
| 483 | - applicationReplicaSetsRefreshButton.setVisible(result); |
|
| 484 | - applicationReplicaSetsCaptionPanel.setVisible(result); |
|
| 485 | - machineImagesCaptionPanel.setVisible(result); |
|
| 486 | - mongoEndpointsCaptionPanel.setVisible(result); |
|
| 487 | - if (result) { |
|
| 488 | - refreshApplicationReplicaSetsTable(); |
|
| 489 | - } |
|
| 478 | + final SafeHtmlCell amiForProxyCell = new SafeHtmlCell(); |
|
| 479 | + final SafeHtmlCell instanceIdCell = new SafeHtmlCell(); |
|
| 480 | + final SafeHtmlCell instancePublicIpCell = new SafeHtmlCell(); |
|
| 481 | + final SafeHtmlCell instancePrivateIpCell = new SafeHtmlCell(); |
|
| 482 | + final Column<ReverseProxyDTO,SafeHtml> amiProxyProxiesColumn = new Column<ReverseProxyDTO, SafeHtml>(amiForProxyCell) { |
|
| 483 | + @Override |
|
| 484 | + public SafeHtml getValue(ReverseProxyDTO proxy) { |
|
| 485 | + return new LinkBuilder() |
|
| 486 | + .setAmiId(proxy.getImageId()) |
|
| 487 | + .setRegion(regionsTable.getSelectionModel().getSelectedObject()) |
|
| 488 | + .setPathMode(LinkBuilder.pathModes.InstanceByAmiIdSearch) |
|
| 489 | + .build(); |
|
| 490 | + } |
|
| 491 | + }; |
|
| 492 | + final Column<ReverseProxyDTO, SafeHtml> instanceIdProxiesColumn = new Column<ReverseProxyDTO, SafeHtml>(instanceIdCell) { |
|
| 493 | + @Override |
|
| 494 | + public SafeHtml getValue(ReverseProxyDTO reverseProxy) { |
|
| 495 | + return new LinkBuilder().setInstanceId(reverseProxy.getInstanceId()).setRegion(regionsTable.getSelectionModel().getSelectedObject()).setPathMode(LinkBuilder.pathModes.InstanceSearch).build(); |
|
| 496 | + } |
|
| 497 | + }; |
|
| 498 | + final Column<ReverseProxyDTO, SafeHtml> instancePublicIpProxiesColumn = new Column<ReverseProxyDTO, SafeHtml>(instancePublicIpCell) { |
|
| 499 | + @Override |
|
| 500 | + public SafeHtml getValue(ReverseProxyDTO reverseProxy) { |
|
| 501 | + return new LinkBuilder().setRegion(regionsTable.getSelectionModel().getSelectedObject()).setPathMode(LinkBuilder.pathModes.publicIp).setPublicIp(reverseProxy.getPublicIpAddress()).build(); |
|
| 502 | + } |
|
| 503 | + }; |
|
| 504 | + final Column<ReverseProxyDTO, SafeHtml> instancePrivateIpProxiesColumn = new Column<ReverseProxyDTO, SafeHtml> (instancePrivateIpCell) { |
|
| 505 | + @Override |
|
| 506 | + public SafeHtml getValue(ReverseProxyDTO reverseProxy) { |
|
| 507 | + return new LinkBuilder().setRegion(regionsTable.getSelectionModel().getSelectedObject()).setPathMode(LinkBuilder.pathModes.privateIp).setPrivateIp(reverseProxy.getPrivateIpAddress()).build(); |
|
| 508 | + } |
|
| 509 | + }; |
|
| 510 | + proxiesTable = new TableWrapperWithMultiSelectionAndFilter<ReverseProxyDTO, StringMessages, AdminConsoleTableResources>( |
|
| 511 | + stringMessages, errorReporter, /* enablePager */ false, |
|
| 512 | + /* entity identity comparator */ Optional.empty(), GWT.create(AdminConsoleTableResources.class), |
|
| 513 | + /* checkbox filter function */ Optional.empty(), /* filter label */ Optional.empty(), |
|
| 514 | + /* filter checkbox label */ null) { |
|
| 515 | + @Override |
|
| 516 | + protected Iterable<String> getSearchableStrings(ReverseProxyDTO reverseProxyDTO) { |
|
| 517 | + final Set<String> result = new HashSet<>(); |
|
| 518 | + if (reverseProxyDTO.getInstanceId() != null) { |
|
| 519 | + result.add(reverseProxyDTO.getInstanceId()); |
|
| 520 | + result.add(reverseProxyDTO.getPrivateIpAddress()); |
|
| 521 | + result.add(reverseProxyDTO.getPublicIpAddress()); |
|
| 522 | + result.add(reverseProxyDTO.getRegion()); |
|
| 490 | 523 | } |
| 524 | + return result; |
|
| 525 | + } |
|
| 526 | + }; |
|
| 527 | + proxiesTable.addColumn(reverseProxyDTO -> reverseProxyDTO.getName(), stringMessages.name()); |
|
| 528 | + proxiesTable.addColumn(instanceIdProxiesColumn, stringMessages.instanceId()); |
|
| 529 | + proxiesTable.addColumn(amiProxyProxiesColumn, stringMessages.id()); |
|
| 530 | + proxiesTable.addColumn(instancePublicIpProxiesColumn , stringMessages.publicIp()); |
|
| 531 | + proxiesTable.addColumn(instancePrivateIpProxiesColumn, stringMessages.privateIp()); |
|
| 532 | + proxiesTable.addColumn(reverseProxyDTO -> reverseProxyDTO.getAvailabilityZoneName(), stringMessages.availabilityZone()); |
|
| 533 | + proxiesTable.addColumn(reverseProxyDTO -> reverseProxyDTO.getHealth(), stringMessages.state()); |
|
| 534 | + //setup actions |
|
| 535 | + final ActionsColumn<ReverseProxyDTO, ReverseProxyImagesBarCell> proxiesActionColumn = new ActionsColumn<ReverseProxyDTO, ReverseProxyImagesBarCell>( |
|
| 536 | + new ReverseProxyImagesBarCell(stringMessages), (revProxy, action) -> true); |
|
| 537 | + proxiesActionColumn.addAction(ReverseProxyImagesBarCell.ACTION_REMOVE, reverseProxy -> { |
|
| 538 | + if (reverseProxy.isDisposable()) { |
|
| 539 | + removeReverseProxy(reverseProxy, reverseProxy.getRegion(), stringMessages); |
|
| 540 | + } else { |
|
| 541 | + errorReporter.reportError(stringMessages.invalidOperationForThisProxy()); |
|
| 542 | + } |
|
| 543 | + }); |
|
| 544 | + proxiesActionColumn.addAction(ReverseProxyImagesBarCell.ACTION_ROTATE_HTTPD_LOGS, reverseProxy -> rotateHttpdLogs(reverseProxy, stringMessages)); |
|
| 545 | + proxiesTable.addColumn(proxiesActionColumn, stringMessages.actions()); |
|
| 546 | + final CaptionPanel proxiesTableCaptionPanel = new CaptionPanel(stringMessages.reverseProxies()); |
|
| 547 | + final VerticalPanel proxiesTableVerticalPanel = new VerticalPanel(); |
|
| 548 | + final HorizontalPanel proxiesTableButtonPanel = new HorizontalPanel(); |
|
| 549 | + //setup buttons above rows |
|
| 550 | + final Button proxiesTableRefreshButton = new Button(stringMessages.refresh()); |
|
| 551 | + final Button proxiesTableAddButton = new Button(stringMessages.add()); |
|
| 552 | + final SelectedElementsCountingButton<ReverseProxyDTO> removeProxiesButton = new SelectedElementsCountingButton<>(stringMessages.remove(), proxiesTable.getSelectionModel(), /* element name mapper */ proxy -> proxy.getName(), |
|
| 553 | + StringMessages.INSTANCE::doYouReallyWantToRemoveSelectedElements, |
|
| 554 | + e -> removeReverseProxies(stringMessages, regionsTable.getSelectionModel().getSelectedObject(), proxiesTable.getSelectionModel().getSelectedSet())); |
|
| 555 | + proxiesTableRefreshButton.addClickHandler(event -> refreshProxiesTable()); |
|
| 556 | + proxiesTableAddButton.addClickHandler(event -> addReverseProxyToCluster(stringMessages, regionsTable.getSelectionModel().getSelectedObject())); |
|
| 557 | + proxiesTableButtonPanel.add(proxiesTableRefreshButton); |
|
| 558 | + proxiesTableButtonPanel.add(proxiesTableAddButton); |
|
| 559 | + proxiesTableButtonPanel.add(removeProxiesButton); |
|
| 560 | + proxiesTableVerticalPanel.add(proxiesTableButtonPanel); |
|
| 561 | + proxiesTableVerticalPanel.add(proxiesTable); |
|
| 562 | + proxiesTableBusy= new SimpleBusyIndicator(); |
|
| 563 | + proxiesTableVerticalPanel.add(proxiesTableBusy); |
|
| 564 | + proxiesTableCaptionPanel.add(proxiesTableVerticalPanel); |
|
| 565 | + mainPanel.add(proxiesTableCaptionPanel); |
|
| 566 | + regionsTable.getSelectionModel().addSelectionChangeHandler(e -> { |
|
| 567 | + final String selectedRegion = regionsTable.getSelectionModel().getSelectedObject(); |
|
| 568 | + refreshAllThatNeedsAwsCredentials(); |
|
| 569 | + storeRegionSelection(userService, selectedRegion); |
|
| 570 | + }); |
|
| 571 | + AsyncCallback<Boolean> validatePassphraseCallback = new AsyncCallback<Boolean>() { |
|
| 572 | + @Override |
|
| 573 | + public void onSuccess(Boolean result) { |
|
| 574 | + sshKeyManagementPanel.setPassphraseValidation(result.booleanValue(), stringMessages); |
|
| 575 | + addApplicationReplicaSetButton.setVisible(result); |
|
| 576 | + applicationReplicaSetsRefreshButton.setVisible(result); |
|
| 577 | + applicationReplicaSetsCaptionPanel.setVisible(result); |
|
| 578 | + machineImagesCaptionPanel.setVisible(result); |
|
| 579 | + mongoEndpointsCaptionPanel.setVisible(result); |
|
| 580 | + proxiesTableCaptionPanel.setVisible(result); |
|
| 581 | + if (result) { |
|
| 582 | + refreshApplicationReplicaSetsTable(); |
|
| 583 | + } |
|
| 584 | + } |
|
| 585 | + |
|
| 586 | + public void onFailure(Throwable caught) { |
|
| 587 | + errorReporter.reportError(stringMessages.passphraseCheckError()); |
|
| 588 | + } |
|
| 589 | + }; |
|
| 590 | + sshKeyManagementPanel.addSshKeySelectionChangedHandler(event -> { |
|
| 591 | + validatePassphrase(stringMessages, validatePassphraseCallback); |
|
| 592 | + }); |
|
| 593 | + sshKeyManagementPanel.addOnPassphraseChangedListener(event -> { |
|
| 594 | + validatePassphrase(stringMessages, validatePassphraseCallback); |
|
| 595 | + }); |
|
| 596 | + validatePassphrase(stringMessages, validatePassphraseCallback); |
|
| 597 | + // TODO try to identify archive servers |
|
| 598 | + // TODO support archive server upgrade |
|
| 599 | + // TODO upon region selection show RabbitMQ, and Central Reverse Proxy clusters in region |
|
| 600 | + } |
|
| 491 | 601 | |
| 492 | - public void onFailure(Throwable caught) { |
|
| 493 | - errorReporter.reportError(stringMessages.passphraseCheckError()); |
|
| 494 | - }; |
|
| 495 | - }; |
|
| 496 | - sshKeyManagementPanel.addSshKeySelectionChangedHandler(event->{ |
|
| 497 | - validatePassphrase(stringMessages, validatePassphraseCallback); |
|
| 498 | - }); |
|
| 499 | - sshKeyManagementPanel.addOnPassphraseChangedListener(event -> { |
|
| 500 | - validatePassphrase(stringMessages, validatePassphraseCallback); |
|
| 501 | - }); |
|
| 502 | - validatePassphrase(stringMessages, validatePassphraseCallback); |
|
| 503 | - // TODO try to identify archive servers |
|
| 504 | - // TODO support archive server upgrade |
|
| 505 | - // TODO upon region selection show RabbitMQ, and Central Reverse Proxy clusters in region |
|
| 506 | - } |
|
| 507 | - |
|
| 508 | 602 | private void openShardManagementPanel(StringMessages stringMessages, String region, SailingApplicationReplicaSetDTO<String> replicaset) { |
| 509 | 603 | new ShardManagementDialog(landscapeManagementService, replicaset, region, errorReporter, stringMessages, new DialogCallback<Boolean>() { |
| 510 | 604 | @Override |
| ... | ... | @@ -1374,10 +1468,162 @@ public class LandscapeManagementPanel extends SimplePanel { |
| 1374 | 1468 | refreshMongoEndpointsTable(); |
| 1375 | 1469 | refreshApplicationReplicaSetsTable(); |
| 1376 | 1470 | refreshMachineImagesTable(); |
| 1471 | + refreshProxiesTable(); |
|
| 1377 | 1472 | sshKeyManagementPanel.showKeysInRegion(mfaLoginWidget.hasValidSessionCredentials() ? |
| 1378 | 1473 | regionsTable.getSelectionModel().getSelectedObject() : null); |
| 1379 | 1474 | } |
| 1475 | + |
|
| 1476 | + private void removeReverseProxies(StringMessages stringMessages, String regionId, |
|
| 1477 | + Iterable<ReverseProxyDTO> reverseProxiesToRemove) { |
|
| 1478 | + Iterator<ReverseProxyDTO> iterator = reverseProxiesToRemove.iterator(); |
|
| 1479 | + while (iterator.hasNext()) { |
|
| 1480 | + removeReverseProxy(iterator.next(), regionId, stringMessages); |
|
| 1481 | + } |
|
| 1482 | + } |
|
| 1483 | + /** |
|
| 1484 | + * Removes a reverse proxy from the cluster and terminates it. |
|
| 1485 | + * @param instance The reverse proxy to remove from the cluster. |
|
| 1486 | + * @param regionId The region the proxy is in. |
|
| 1487 | + */ |
|
| 1488 | + private void removeReverseProxy(ReverseProxyDTO instance, String regionId, StringMessages stringMessages) { |
|
| 1489 | + if (sshKeyManagementPanel.getSelectedKeyPair() == null) { |
|
| 1490 | + Notification.notify(stringMessages.pleaseSelectSshKeyPair(), NotificationType.INFO); |
|
| 1491 | + } else { |
|
| 1492 | + proxiesTableBusy.setBusy(true); |
|
| 1493 | + landscapeManagementService.removeReverseProxy(instance, regionId, |
|
| 1494 | + sshKeyManagementPanel.getSelectedKeyPair().getName(), |
|
| 1495 | + sshKeyManagementPanel.getPassphraseForPrivateKeyDecryption() != null |
|
| 1496 | + ? sshKeyManagementPanel.getPassphraseForPrivateKeyDecryption().getBytes() |
|
| 1497 | + : null, |
|
| 1498 | + new AsyncCallback<Void>() { |
|
| 1499 | + |
|
| 1500 | + @Override |
|
| 1501 | + public void onFailure(Throwable caught) { |
|
| 1502 | + errorReporter.reportError(caught.getMessage()); |
|
| 1503 | + proxiesTableBusy.setBusy(false); |
|
| 1504 | + |
|
| 1505 | + } |
|
| 1506 | + |
|
| 1507 | + @Override |
|
| 1508 | + public void onSuccess(Void result) { |
|
| 1509 | + proxiesTableBusy.setBusy(false); |
|
| 1510 | + refreshProxiesTable(); |
|
| 1511 | + } |
|
| 1512 | + |
|
| 1513 | + }); |
|
| 1514 | + } |
|
| 1515 | + } |
|
| 1380 | 1516 | |
| 1517 | + /** |
|
| 1518 | + * Creates a reverse proxy based on the user's input. |
|
| 1519 | + * @param region The region to spawn the reverse proxy in. |
|
| 1520 | + */ |
|
| 1521 | + private void addReverseProxyToCluster(StringMessages stringMessages, String region) { |
|
| 1522 | + if (sshKeyManagementPanel.getSelectedKeyPair() == null || regionsTable.getSelectionModel().getSelectedObject() == null) { |
|
| 1523 | + Notification.notify(stringMessages.pleaseSelectSshKeyPair(), NotificationType.INFO); |
|
| 1524 | + } else { |
|
| 1525 | + landscapeManagementService.describeAvailabilityZones(regionsTable.getSelectionModel().getSelectedObject(), |
|
| 1526 | + new AsyncCallback<ArrayList<AvailabilityZoneDTO>>() { |
|
| 1527 | + @Override |
|
| 1528 | + public void onSuccess(ArrayList<AvailabilityZoneDTO> result) { |
|
| 1529 | + showReverseProxyDialog(stringMessages, region, result); |
|
| 1530 | + } |
|
| 1531 | + |
|
| 1532 | + @Override |
|
| 1533 | + public void onFailure(Throwable caught) { |
|
| 1534 | + errorReporter.reportError(caught.getMessage()); |
|
| 1535 | + } |
|
| 1536 | + }); |
|
| 1537 | + } |
|
| 1538 | + } |
|
| 1539 | + |
|
| 1540 | + private void showReverseProxyDialog(StringMessages stringMessages, String region, ArrayList<AvailabilityZoneDTO> availabilityZones) { |
|
| 1541 | + new CreateReverseProxyInClusterDialog(stringMessages, errorReporter, landscapeManagementService, region, |
|
| 1542 | + proxiesTable.getTable().getVisibleItems(), availabilityZones, |
|
| 1543 | + new DialogCallback<CreateReverseProxyInClusterDialog.CreateReverseProxyInstructions>() { |
|
| 1544 | + @Override |
|
| 1545 | + public void ok(CreateReverseProxyInClusterDialog.CreateReverseProxyInstructions editedObject) { |
|
| 1546 | + editedObject.setKey(sshKeyManagementPanel.getSelectedKeyPair() == null ? null |
|
| 1547 | + : sshKeyManagementPanel.getSelectedKeyPair().getName()); |
|
| 1548 | + proxiesTableBusy.setBusy(true); |
|
| 1549 | + landscapeManagementService.addReverseProxy(editedObject.getName(), |
|
| 1550 | + editedObject.getInstanceType(), editedObject.getRegion(), editedObject.getKey(), editedObject.getAvailabilityZoneDTO(), |
|
| 1551 | + new AsyncCallback<Void>() { |
|
| 1552 | + @Override |
|
| 1553 | + public void onSuccess(Void result) { |
|
| 1554 | + Notification.notify(stringMessages.success(), NotificationType.SUCCESS); |
|
| 1555 | + proxiesTableBusy.setBusy(false); |
|
| 1556 | + refreshProxiesTable(); |
|
| 1557 | + } |
|
| 1558 | + |
|
| 1559 | + @Override |
|
| 1560 | + public void onFailure(Throwable caught) { |
|
| 1561 | + proxiesTableBusy.setBusy(false); |
|
| 1562 | + errorReporter.reportError(caught.getMessage()); |
|
| 1563 | + } |
|
| 1564 | + }); |
|
| 1565 | + } |
|
| 1566 | + |
|
| 1567 | + @Override |
|
| 1568 | + public void cancel() { |
|
| 1569 | + } |
|
| 1570 | + }).show(); |
|
| 1571 | + } |
|
| 1572 | + |
|
| 1573 | + /** |
|
| 1574 | + * Updates the proxies table with all the reverse proxies in the region. |
|
| 1575 | + */ |
|
| 1576 | + private void refreshProxiesTable() { |
|
| 1577 | + proxiesTable.getFilterPanel().removeAll(); |
|
| 1578 | + if (mfaLoginWidget.hasValidSessionCredentials() |
|
| 1579 | + && regionsTable.getSelectionModel().getSelectedObject() != null) { |
|
| 1580 | + proxiesTableBusy.setBusy(true); |
|
| 1581 | + landscapeManagementService.getReverseProxies(regionsTable.getSelectionModel().getSelectedObject(), |
|
| 1582 | + new AsyncCallback<ArrayList<ReverseProxyDTO>>() { |
|
| 1583 | + @Override |
|
| 1584 | + public void onFailure(Throwable caught) { |
|
| 1585 | + errorReporter.reportError(caught.getMessage()); |
|
| 1586 | + proxiesTableBusy.setBusy(false); |
|
| 1587 | + } |
|
| 1588 | + |
|
| 1589 | + @Override |
|
| 1590 | + public void onSuccess(ArrayList<ReverseProxyDTO> reverseProxyDTOs) { |
|
| 1591 | + proxiesTable.refresh(reverseProxyDTOs); |
|
| 1592 | + proxiesTableBusy.setBusy(false); |
|
| 1593 | + } |
|
| 1594 | + }); |
|
| 1595 | + } |
|
| 1596 | + } |
|
| 1597 | + |
|
| 1598 | + /** |
|
| 1599 | + * Rotates the httpd logs on an instance. |
|
| 1600 | + * @param reverseProxy The instance to rotate the logs on. |
|
| 1601 | + */ |
|
| 1602 | + private void rotateHttpdLogs(ReverseProxyDTO reverseProxy, StringMessages stringMessages) { |
|
| 1603 | + if (sshKeyManagementPanel.getSelectedKeyPair() == null) { |
|
| 1604 | + Notification.notify(stringMessages.pleaseSelectSshKeyPair(), NotificationType.INFO); |
|
| 1605 | + } else { |
|
| 1606 | + landscapeManagementService.rotateHttpdLogs(reverseProxy, reverseProxy.getRegion(), |
|
| 1607 | + sshKeyManagementPanel.getSelectedKeyPair().getName(), |
|
| 1608 | + sshKeyManagementPanel.getPassphraseForPrivateKeyDecryption() != null |
|
| 1609 | + ? sshKeyManagementPanel.getPassphraseForPrivateKeyDecryption().getBytes() |
|
| 1610 | + : null, |
|
| 1611 | + new AsyncCallback<Void>() { |
|
| 1612 | + @Override |
|
| 1613 | + public void onFailure(Throwable caught) { |
|
| 1614 | + errorReporter.reportError(caught.getMessage()); |
|
| 1615 | + } |
|
| 1616 | + |
|
| 1617 | + @Override |
|
| 1618 | + public void onSuccess(Void result) { |
|
| 1619 | + Notification.notify( |
|
| 1620 | + stringMessages.successfullyRotatedHttpdLogsOnInstance(reverseProxy.getInstanceId()), |
|
| 1621 | + NotificationType.SUCCESS); |
|
| 1622 | + } |
|
| 1623 | + }); |
|
| 1624 | + } |
|
| 1625 | + } |
|
| 1626 | + |
|
| 1381 | 1627 | private void storeRegionSelection(UserService userService, String selectedRegion) { |
| 1382 | 1628 | if (selectedRegion != null) { |
| 1383 | 1629 | userService.setPreference(AWS_DEFAULT_REGION_USER_PREFERENCE, selectedRegion, new AsyncCallback<Void>() { |
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/LandscapeManagementWriteService.java
| ... | ... | @@ -6,6 +6,7 @@ import java.util.Map; |
| 6 | 6 | import com.google.gwt.user.client.rpc.RemoteService; |
| 7 | 7 | import com.sap.sailing.domain.common.DataImportProgress; |
| 8 | 8 | import com.sap.sailing.landscape.ui.shared.AmazonMachineImageDTO; |
| 9 | +import com.sap.sailing.landscape.ui.shared.AvailabilityZoneDTO; |
|
| 9 | 10 | import com.sap.sailing.landscape.ui.shared.AwsInstanceDTO; |
| 10 | 11 | import com.sap.sailing.landscape.ui.shared.AwsShardDTO; |
| 11 | 12 | import com.sap.sailing.landscape.ui.shared.CompareServersResultDTO; |
| ... | ... | @@ -14,6 +15,7 @@ import com.sap.sailing.landscape.ui.shared.MongoEndpointDTO; |
| 14 | 15 | import com.sap.sailing.landscape.ui.shared.MongoScalingInstructionsDTO; |
| 15 | 16 | import com.sap.sailing.landscape.ui.shared.ProcessDTO; |
| 16 | 17 | import com.sap.sailing.landscape.ui.shared.ReleaseDTO; |
| 18 | +import com.sap.sailing.landscape.ui.shared.ReverseProxyDTO; |
|
| 17 | 19 | import com.sap.sailing.landscape.ui.shared.SSHKeyPairDTO; |
| 18 | 20 | import com.sap.sailing.landscape.ui.shared.SailingApplicationReplicaSetDTO; |
| 19 | 21 | import com.sap.sailing.landscape.ui.shared.SerializationDummyDTO; |
| ... | ... | @@ -28,6 +30,16 @@ public interface LandscapeManagementWriteService extends RemoteService { |
| 28 | 30 | |
| 29 | 31 | ArrayList<MongoEndpointDTO> getMongoEndpoints(String region) throws Exception; |
| 30 | 32 | |
| 33 | + ArrayList<ReverseProxyDTO> getReverseProxies(String region) throws Exception; |
|
| 34 | + |
|
| 35 | + void removeReverseProxy(ReverseProxyDTO instance, String region, String optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception; |
|
| 36 | + |
|
| 37 | + void rotateHttpdLogs(ReverseProxyDTO proxy, String region, String optionalKeyName, byte[] passphraseForPrivateKeyDecryption)throws Exception; |
|
| 38 | + |
|
| 39 | + void addReverseProxy(String instanceName, String instanceType, String region, String launchKey, AvailabilityZoneDTO availabilityZoneDTO); |
|
| 40 | + |
|
| 41 | + ArrayList<AvailabilityZoneDTO> describeAvailabilityZones(String region); |
|
| 42 | + |
|
| 31 | 43 | MongoEndpointDTO getMongoEndpoint(String region, String replicaSetName) throws Exception; |
| 32 | 44 | |
| 33 | 45 | ArrayList<SSHKeyPairDTO> getSshKeys(String regionId); |
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/LandscapeManagementWriteServiceAsync.java
| ... | ... | @@ -2,13 +2,13 @@ package com.sap.sailing.landscape.ui.client; |
| 2 | 2 | |
| 3 | 3 | import java.util.ArrayList; |
| 4 | 4 | import java.util.Map; |
| 5 | -import java.util.Optional; |
|
| 6 | 5 | |
| 7 | 6 | import com.google.gwt.user.client.rpc.AsyncCallback; |
| 8 | 7 | import com.sap.sailing.domain.common.DataImportProgress; |
| 9 | 8 | import com.sap.sailing.landscape.SailingAnalyticsHost; |
| 10 | 9 | import com.sap.sailing.landscape.common.SharedLandscapeConstants; |
| 11 | 10 | import com.sap.sailing.landscape.ui.shared.AmazonMachineImageDTO; |
| 11 | +import com.sap.sailing.landscape.ui.shared.AvailabilityZoneDTO; |
|
| 12 | 12 | import com.sap.sailing.landscape.ui.shared.AwsInstanceDTO; |
| 13 | 13 | import com.sap.sailing.landscape.ui.shared.AwsShardDTO; |
| 14 | 14 | import com.sap.sailing.landscape.ui.shared.CompareServersResultDTO; |
| ... | ... | @@ -17,6 +17,7 @@ import com.sap.sailing.landscape.ui.shared.MongoEndpointDTO; |
| 17 | 17 | import com.sap.sailing.landscape.ui.shared.MongoScalingInstructionsDTO; |
| 18 | 18 | import com.sap.sailing.landscape.ui.shared.ProcessDTO; |
| 19 | 19 | import com.sap.sailing.landscape.ui.shared.ReleaseDTO; |
| 20 | +import com.sap.sailing.landscape.ui.shared.ReverseProxyDTO; |
|
| 20 | 21 | import com.sap.sailing.landscape.ui.shared.SSHKeyPairDTO; |
| 21 | 22 | import com.sap.sailing.landscape.ui.shared.SailingApplicationReplicaSetDTO; |
| 22 | 23 | import com.sap.sailing.landscape.ui.shared.SerializationDummyDTO; |
| ... | ... | @@ -33,7 +34,34 @@ public interface LandscapeManagementWriteServiceAsync { |
| 33 | 34 | |
| 34 | 35 | void getMongoEndpoint(String region, String replicaSetName, |
| 35 | 36 | AsyncCallback<MongoEndpointDTO> callback); |
| 36 | - |
|
| 37 | + |
|
| 38 | + void getReverseProxies(String regionId, AsyncCallback<ArrayList<ReverseProxyDTO>> callback); |
|
| 39 | + |
|
| 40 | + /** |
|
| 41 | + * Removes a reverse proxy from the given cluster and terminates it. |
|
| 42 | + * @return Returns true if a success. |
|
| 43 | + */ |
|
| 44 | + void removeReverseProxy(ReverseProxyDTO instance, String region, String optionalKeyName, byte[] privateKeyEncryptionPassphrase, AsyncCallback<Void> callback); |
|
| 45 | + |
|
| 46 | + /** |
|
| 47 | + * Rotates the httpd logs on a proxy instance. |
|
| 48 | + * @param optionalKeyName Name of key used to connect to the instance to restart |
|
| 49 | + * @param passphraseForPrivateKeyDecryption the passphrase for the given key |
|
| 50 | + */ |
|
| 51 | + void rotateHttpdLogs(ReverseProxyDTO proxy, String region, String optionalKeyName, |
|
| 52 | + byte[] passphraseForPrivateKeyDecryption, AsyncCallback<Void> callback); |
|
| 53 | + |
|
| 54 | + /** |
|
| 55 | + * Adds a reverse proxy to the cluster and the right load balancer's target group. |
|
| 56 | + */ |
|
| 57 | + void addReverseProxy(String instanceName, String instanceType, String region, String launchKey, AvailabilityZoneDTO availabilityZoneDTO, AsyncCallback<Void> callback); |
|
| 58 | + |
|
| 59 | + |
|
| 60 | + /** |
|
| 61 | + * Gets all availability zones in a region. |
|
| 62 | + */ |
|
| 63 | + void describeAvailabilityZones(String region,AsyncCallback<ArrayList<AvailabilityZoneDTO>> asyncCallback); |
|
| 64 | + |
|
| 37 | 65 | /** |
| 38 | 66 | * The calling subject will see only those keys for which it has the {@code READ} permission. |
| 39 | 67 | */ |
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/LinkBuilder.java
| ... | ... | @@ -10,7 +10,7 @@ public class LinkBuilder implements Builder<LinkBuilder, SafeHtml> { |
| 10 | 10 | |
| 11 | 11 | static public enum pathModes { |
| 12 | 12 | InstanceSearch, InstanceByAmiIdSearch, ImageSearch, AmiSearch, Hostname, |
| 13 | - ReplicaLinks, Version, MasterHost, TargetGroupSearch, AutoScalingGroupSearch |
|
| 13 | + ReplicaLinks, Version, MasterHost, TargetGroupSearch, AutoScalingGroupSearch, publicIp, privateIp |
|
| 14 | 14 | }; |
| 15 | 15 | |
| 16 | 16 | private pathModes pathMode; |
| ... | ... | @@ -20,6 +20,8 @@ public class LinkBuilder implements Builder<LinkBuilder, SafeHtml> { |
| 20 | 20 | private String targetGroupName; |
| 21 | 21 | private String autoScalingGroupName; |
| 22 | 22 | private String amiId; |
| 23 | + private String publicIp; |
|
| 24 | + private String privateIp; |
|
| 23 | 25 | |
| 24 | 26 | LinkBuilder setPathMode(pathModes mode) { |
| 25 | 27 | pathMode = mode; |
| ... | ... | @@ -51,6 +53,16 @@ public class LinkBuilder implements Builder<LinkBuilder, SafeHtml> { |
| 51 | 53 | return self(); |
| 52 | 54 | } |
| 53 | 55 | |
| 56 | + LinkBuilder setPublicIp(String ip) { |
|
| 57 | + this.publicIp = ip; |
|
| 58 | + return self(); |
|
| 59 | + } |
|
| 60 | + |
|
| 61 | + LinkBuilder setPrivateIp(String ip) { |
|
| 62 | + this.privateIp = ip; |
|
| 63 | + return self(); |
|
| 64 | + } |
|
| 65 | + |
|
| 54 | 66 | LinkBuilder setReplicaSet(SailingApplicationReplicaSetDTO<String> replicaSet) { |
| 55 | 67 | this.replicaSet = replicaSet; |
| 56 | 68 | return self(); |
| ... | ... | @@ -119,6 +131,13 @@ public class LinkBuilder implements Builder<LinkBuilder, SafeHtml> { |
| 119 | 131 | return "https://releases.sapsailing.com/" + version + "/release-notes.txt"; |
| 120 | 132 | } |
| 121 | 133 | |
| 134 | + /** |
|
| 135 | + * Checks if an attribute is null and throws an exception if so. |
|
| 136 | + * |
|
| 137 | + * @param attr The variable name to check for null. |
|
| 138 | + * @param name The human readable name of the variable. |
|
| 139 | + * @throws Exception This stores the value which is not set. |
|
| 140 | + */ |
|
| 122 | 141 | private void checkAttribute(Object attr, String name) throws Exception { |
| 123 | 142 | if (attr == null) { |
| 124 | 143 | throw new Exception(name + " needs to be given!"); |
| ... | ... | @@ -195,6 +214,20 @@ public class LinkBuilder implements Builder<LinkBuilder, SafeHtml> { |
| 195 | 214 | checkAttribute(autoScalingGroupName, "Auto-Scaling Group Name"); |
| 196 | 215 | appendEc2AutoScalingGroupLink(builder, autoScalingGroupName); |
| 197 | 216 | break; |
| 217 | + case publicIp: |
|
| 218 | + checkAttribute(publicIp, "Public Ip"); |
|
| 219 | + final String ipLink = "http://" + publicIp; |
|
| 220 | + builder.appendHtmlConstant("<a target=\"_blank\" href=\"" + ipLink + "\">"); |
|
| 221 | + builder.appendEscaped(publicIp); |
|
| 222 | + builder.appendHtmlConstant("</a>"); |
|
| 223 | + break; |
|
| 224 | + case privateIp: |
|
| 225 | + checkAttribute(privateIp, "Private Ip"); |
|
| 226 | + final String privateIpLink = "http://" + privateIp; |
|
| 227 | + builder.appendHtmlConstant("<a target=\"_blank\" href=\"" + privateIpLink + "\">"); |
|
| 228 | + builder.appendEscaped(privateIp); |
|
| 229 | + builder.appendHtmlConstant("</a>"); |
|
| 230 | + break; |
|
| 198 | 231 | } |
| 199 | 232 | } catch (Exception e) { |
| 200 | 233 | builder.appendHtmlConstant("<a target=\"_blank\" >"); |
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/ReverseProxyImagesBarCell.java
| ... | ... | @@ -0,0 +1,32 @@ |
| 1 | +package com.sap.sailing.landscape.ui.client; |
|
| 2 | + |
|
| 3 | +import java.util.Arrays; |
|
| 4 | + |
|
| 5 | +import com.google.gwt.text.shared.SafeHtmlRenderer; |
|
| 6 | +import com.sap.sailing.landscape.ui.client.i18n.StringMessages; |
|
| 7 | +import com.sap.sse.gwt.client.IconResources; |
|
| 8 | +import com.sap.sse.gwt.client.celltable.ImagesBarCell; |
|
| 9 | +import com.sap.sse.security.shared.HasPermissions.DefaultActions; |
|
| 10 | + |
|
| 11 | +public class ReverseProxyImagesBarCell extends ImagesBarCell{ |
|
| 12 | + static final String ACTION_REMOVE = DefaultActions.DELETE.name(); |
|
| 13 | + static final String ACTION_ROTATE_HTTPD_LOGS = "Rotate httpd logs"; |
|
| 14 | + |
|
| 15 | + private final StringMessages stringMessages; |
|
| 16 | + |
|
| 17 | + public ReverseProxyImagesBarCell(StringMessages stringMessages) { |
|
| 18 | + super(); |
|
| 19 | + this.stringMessages = stringMessages; |
|
| 20 | + } |
|
| 21 | + |
|
| 22 | + public ReverseProxyImagesBarCell(SafeHtmlRenderer<String> renderer, StringMessages stringMessages) { |
|
| 23 | + super(); |
|
| 24 | + this.stringMessages = stringMessages; |
|
| 25 | + } |
|
| 26 | + |
|
| 27 | + @Override |
|
| 28 | + protected Iterable<ImageSpec> getImageSpecs() { |
|
| 29 | + return Arrays.asList(new ImageSpec(ACTION_REMOVE, stringMessages.remove(), IconResources.INSTANCE.removeIcon()), |
|
| 30 | + new ImageSpec(ACTION_ROTATE_HTTPD_LOGS, stringMessages.rotateHttpdLogs(), IconResources.INSTANCE.refreshIcon())); |
|
| 31 | + } |
|
| 32 | +} |
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/i18n/StringMessages.java
| ... | ... | @@ -52,6 +52,7 @@ com.sap.sse.gwt.adminconsole.StringMessages { |
| 52 | 52 | String priority(); |
| 53 | 53 | String votes(); |
| 54 | 54 | String instanceType(); |
| 55 | + String instanceName(); |
|
| 55 | 56 | String youHaveToProvideAPositiveNumberOfInstancesToLaunch(); |
| 56 | 57 | String youHaveToProvideANonNegativePriority(); |
| 57 | 58 | String youHaveToProvideANonNegativeNumberOfVotes(); |
| ... | ... | @@ -166,4 +167,14 @@ com.sap.sse.gwt.adminconsole.StringMessages { |
| 166 | 167 | String dnsNameAlreadyInUse(); |
| 167 | 168 | String errorArchivingMongoDBTo(String replicaSetNameToArchiveTo, String mongoDBArchivingErrorMessage); |
| 168 | 169 | String optionalSessionToken(); |
| 170 | + String reverseProxies(); |
|
| 171 | + String rotateHttpdLogs(); |
|
| 172 | + String successfullyRotatedHttpdLogsOnInstance(String instance); |
|
| 173 | + String invalidOperationForThisProxy(); |
|
| 174 | + String pleaseProvideNonEmptyNameAndAZ(); |
|
| 175 | + String success(); |
|
| 176 | + String availabilityZone(); |
|
| 177 | + String runOnExisting(); |
|
| 178 | + String publicIp(); |
|
| 179 | + String privateIp(); |
|
| 169 | 180 | } |
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/i18n/StringMessages.properties
| ... | ... | @@ -41,6 +41,7 @@ numberOfMongoInstancesToLaunch=Number of MongoDB Instances to Launch |
| 41 | 41 | priority=Priority |
| 42 | 42 | votes=Votes |
| 43 | 43 | instanceType=Instance Type |
| 44 | +instanceName=Instance Name |
|
| 44 | 45 | youHaveToProvideAPositiveNumberOfInstancesToLaunch=You have to provide a positive number of instances to launch. |
| 45 | 46 | youHaveToProvideANonNegativePriority=You have to provide a non-negative priority. |
| 46 | 47 | youHaveToProvideANonNegativeNumberOfVotes=You have to provide a non-negative number of votes. |
| ... | ... | @@ -154,4 +155,14 @@ processOfReplicaSet=on port {0} (application replica set {1}) |
| 154 | 155 | successfullyMovedAllProcessesAwayFromHost=Successfully moved all processes away from host {0} |
| 155 | 156 | dnsNameAlreadyInUse=DNS name already in use |
| 156 | 157 | errorArchivingMongoDBTo=Error archiving replica set''s MongoDB to the Mongo replica set {0}: {1} |
| 157 | -optionalSessionToken=Session Token (optional) |
|
| ... | ... | \ No newline at end of file |
| 0 | +optionalSessionToken=Session Token (optional) |
|
| 1 | +reverseProxies=Reverse proxies |
|
| 2 | +rotateHttpdLogs=Rotate httpd logs |
|
| 3 | +successfullyRotatedHttpdLogsOnInstance=Successfully rotated the Apache Httpd logs on instance: {0} |
|
| 4 | +invalidOperationForThisProxy=You can''t perform this operation on this instance |
|
| 5 | +pleaseProvideNonEmptyNameAndAZ=Please provide a non-empty name (UTF-8) and an AZ. |
|
| 6 | +success=success |
|
| 7 | +availabilityZone=Availability Zone |
|
| 8 | +runOnExisting=Run on an existing, running instance |
|
| 9 | +publicIp=Public IP address |
|
| 10 | +privateIp=Private IP address |
|
| ... | ... | \ No newline at end of file |
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/i18n/StringMessages_de.properties
| ... | ... | @@ -41,6 +41,7 @@ numberOfMongoInstancesToLaunch=Anzahl zu startender MongoDB-Instanzen |
| 41 | 41 | priority=Priorität |
| 42 | 42 | votes=Stimmen |
| 43 | 43 | instanceType=Instanz-Typ |
| 44 | +instanceName=Instanz-Name |
|
| 44 | 45 | youHaveToProvideAPositiveNumberOfInstancesToLaunch=Die Anzahl der zu startenden Instanzen muss größer 0 sein. |
| 45 | 46 | youHaveToProvideANonNegativePriority=Die Priorität darf nicht negativ sein. |
| 46 | 47 | youHaveToProvideANonNegativeNumberOfVotes=Die Stimmenanzahl darf nicht negativ sein. |
| ... | ... | @@ -153,4 +154,14 @@ processOfReplicaSet=auf Port {0} (Anwendungs-Cluster {1}) |
| 153 | 154 | successfullyMovedAllProcessesAwayFromHost=Alle Prozesse erfolgreich von Server {0} verschoben |
| 154 | 155 | dnsNameAlreadyInUse=DNS-Name wird bereits benutzt |
| 155 | 156 | errorArchivingMongoDBTo=Fehler beim Archivieren der Datenbank ins MongoDB Replica-Set {0}: {1} |
| 156 | -optionalSessionToken=Sitzungsschlüssel (optional) |
|
| ... | ... | \ No newline at end of file |
| 0 | +optionalSessionToken=Sitzungsschlüssel (optional) |
|
| 1 | +reverseProxies=Reverse Proxys |
|
| 2 | +rotateHttpdLogs=logrotate für Instanz |
|
| 3 | +successfullyRotatedHttpdLogsOnInstance=Logrotate erfolgreich. Instanz: {0} |
|
| 4 | +invalidOperationForThisProxy=Operation verboten |
|
| 5 | +pleaseProvideNonEmptyNameAndAZ=Bitte den Namen der Instanz angeben (UTF-8) & AZ. |
|
| 6 | +success=Erfolg |
|
| 7 | +availabilityZone=Availability Zone |
|
| 8 | +runOnExisting=Auf anderem laufenden Server ausführen |
|
| 9 | +publicIp=Öffentlich IP-Adresse |
|
| 10 | +privateIP=Private IP-Adresse |
|
| ... | ... | \ No newline at end of file |
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/server/LandscapeManagementWriteServiceImpl.java
| ... | ... | @@ -50,6 +50,7 @@ import com.sap.sailing.landscape.procedures.UpgradeAmi; |
| 50 | 50 | import com.sap.sailing.landscape.ui.client.LandscapeManagementWriteService; |
| 51 | 51 | import com.sap.sailing.landscape.ui.impl.Activator; |
| 52 | 52 | import com.sap.sailing.landscape.ui.shared.AmazonMachineImageDTO; |
| 53 | +import com.sap.sailing.landscape.ui.shared.AvailabilityZoneDTO; |
|
| 53 | 54 | import com.sap.sailing.landscape.ui.shared.AwsInstanceDTO; |
| 54 | 55 | import com.sap.sailing.landscape.ui.shared.AwsShardDTO; |
| 55 | 56 | import com.sap.sailing.landscape.ui.shared.CompareServersResultDTO; |
| ... | ... | @@ -59,6 +60,7 @@ import com.sap.sailing.landscape.ui.shared.MongoProcessDTO; |
| 59 | 60 | import com.sap.sailing.landscape.ui.shared.MongoScalingInstructionsDTO; |
| 60 | 61 | import com.sap.sailing.landscape.ui.shared.ProcessDTO; |
| 61 | 62 | import com.sap.sailing.landscape.ui.shared.ReleaseDTO; |
| 63 | +import com.sap.sailing.landscape.ui.shared.ReverseProxyDTO; |
|
| 62 | 64 | import com.sap.sailing.landscape.ui.shared.SSHKeyPairDTO; |
| 63 | 65 | import com.sap.sailing.landscape.ui.shared.SailingAnalyticsProcessDTO; |
| 64 | 66 | import com.sap.sailing.landscape.ui.shared.SailingApplicationReplicaSetDTO; |
| ... | ... | @@ -84,8 +86,11 @@ import com.sap.sse.landscape.aws.AwsInstance; |
| 84 | 86 | import com.sap.sse.landscape.aws.AwsLandscape; |
| 85 | 87 | import com.sap.sse.landscape.aws.AwsShard; |
| 86 | 88 | import com.sap.sse.landscape.aws.HostSupplier; |
| 89 | +import com.sap.sse.landscape.aws.LandscapeConstants; |
|
| 90 | +import com.sap.sse.landscape.aws.TargetGroup; |
|
| 87 | 91 | import com.sap.sse.landscape.aws.common.shared.PlainRedirectDTO; |
| 88 | 92 | import com.sap.sse.landscape.aws.common.shared.RedirectDTO; |
| 93 | +import com.sap.sse.landscape.aws.impl.ApacheReverseProxy; |
|
| 89 | 94 | import com.sap.sse.landscape.aws.impl.AwsAvailabilityZoneImpl; |
| 90 | 95 | import com.sap.sse.landscape.aws.impl.AwsInstanceImpl; |
| 91 | 96 | import com.sap.sse.landscape.aws.impl.AwsRegion; |
| ... | ... | @@ -112,6 +117,9 @@ import com.sap.sse.util.ThreadPoolUtil; |
| 112 | 117 | import software.amazon.awssdk.services.ec2.model.AvailabilityZone; |
| 113 | 118 | import software.amazon.awssdk.services.ec2.model.InstanceType; |
| 114 | 119 | import software.amazon.awssdk.services.ec2.model.KeyPairInfo; |
| 120 | +import software.amazon.awssdk.services.elasticloadbalancingv2.model.Tag; |
|
| 121 | +import software.amazon.awssdk.services.elasticloadbalancingv2.model.TagDescription; |
|
| 122 | +import software.amazon.awssdk.services.elasticloadbalancingv2.model.TargetHealth; |
|
| 115 | 123 | import software.amazon.awssdk.services.route53.model.ResourceRecordSet; |
| 116 | 124 | |
| 117 | 125 | public class LandscapeManagementWriteServiceImpl extends ResultCachingProxiedRemoteServiceServlet |
| ... | ... | @@ -230,6 +238,104 @@ public class LandscapeManagementWriteServiceImpl extends ResultCachingProxiedRem |
| 230 | 238 | return result; |
| 231 | 239 | } |
| 232 | 240 | |
| 241 | + @Override |
|
| 242 | + public ArrayList<AvailabilityZoneDTO> describeAvailabilityZones(String region) { |
|
| 243 | + final ArrayList<AvailabilityZoneDTO> availabilityZones = new ArrayList<>(); |
|
| 244 | + getLandscape().getAvailabilityZones(new AwsRegion(region, getLandscape())) |
|
| 245 | + .forEach(az -> availabilityZones.add(new AvailabilityZoneDTO(az.getName(), region, az.getId()))); |
|
| 246 | + return availabilityZones; |
|
| 247 | + } |
|
| 248 | + |
|
| 249 | + @Override |
|
| 250 | + public ArrayList<ReverseProxyDTO> getReverseProxies(String region) throws Exception { |
|
| 251 | + checkLandscapeManageAwsPermission(); |
|
| 252 | + final AwsLandscape<String> landscape = getLandscape(); |
|
| 253 | + TargetGroup<String> targetGroupInQuestion = null; |
|
| 254 | + for (TargetGroup<String> targetGroup : landscape.getTargetGroups(new AwsRegion(region, landscape))) { |
|
| 255 | + for (TagDescription description : targetGroup.getTagDescriptions()) { // There should only be 1 tag |
|
| 256 | + // description. |
|
| 257 | + if (!description.tags().isEmpty()) { |
|
| 258 | + for (Tag tag : description.tags()) { |
|
| 259 | + if (tag.key().equals(LandscapeConstants.ALL_REVERSE_PROXIES) |
|
| 260 | + && targetGroup.getLoadBalancerArn() != null |
|
| 261 | + && !targetGroup.getLoadBalancerArn().contains(LandscapeConstants.NLB_ARN_CONTAINS)) { |
|
| 262 | + targetGroupInQuestion = targetGroup; |
|
| 263 | + } |
|
| 264 | + } |
|
| 265 | + } |
|
| 266 | + } |
|
| 267 | + } |
|
| 268 | + Map<AwsInstance<String>, TargetHealth> healths = null; |
|
| 269 | + if (targetGroupInQuestion != null) { |
|
| 270 | + healths = landscape.getTargetHealthDescriptions(targetGroupInQuestion); |
|
| 271 | + } |
|
| 272 | + final ArrayList<ReverseProxyDTO> results = new ArrayList<>(); |
|
| 273 | + for (AwsInstance<String> instance : landscape.getReverseProxyCluster(new AwsRegion(region, landscape)) |
|
| 274 | + .getHosts()) { |
|
| 275 | + boolean isDisposable = landscape.getTag(instance, LandscapeConstants.DISPOSABLE_PROXY).isPresent() ? true : false; |
|
| 276 | + ReverseProxyDTO dto = convertToReverseProxyDTO(region, healths, instance, isDisposable); |
|
| 277 | + results.add(dto); |
|
| 278 | + } |
|
| 279 | + return results; |
|
| 280 | + } |
|
| 281 | + |
|
| 282 | + private ReverseProxyDTO convertToReverseProxyDTO(String region, Map<AwsInstance<String>, TargetHealth> healths, |
|
| 283 | + AwsInstance<String> instance, boolean isDisposable) { |
|
| 284 | + return new ReverseProxyDTO(instance.getInstanceId(), |
|
| 285 | + instance.getPrivateAddress().getHostAddress(), instance.getPublicAddress().getHostAddress(), |
|
| 286 | + region, instance.getLaunchTimePoint(), instance.isSharedHost(), |
|
| 287 | + instance.getNameTag(), instance.getImageId(), extractHealth(healths, instance), |
|
| 288 | + isDisposable, new AvailabilityZoneDTO(instance.getAvailabilityZone().getName(), instance.getRegion().getId(), instance.getAvailabilityZone().getId())); |
|
| 289 | + } |
|
| 290 | + |
|
| 291 | + @Override |
|
| 292 | + public void rotateHttpdLogs(ReverseProxyDTO instanceDTO, String region, String optionalKeyName, |
|
| 293 | + byte[] passphraseForPrivateKeyDecryption) throws Exception { |
|
| 294 | + checkLandscapeManageAwsPermission(); |
|
| 295 | + AwsInstance<String> awsInstance = getLandscape().getHostByInstanceId(new AwsRegion(region, getLandscape()), |
|
| 296 | + instanceDTO.getInstanceId(), AwsInstanceImpl::new); |
|
| 297 | + new ApacheReverseProxy<>(getLandscape(), awsInstance).rotateLogs(Optional.ofNullable(optionalKeyName), |
|
| 298 | + passphraseForPrivateKeyDecryption); |
|
| 299 | + } |
|
| 300 | + |
|
| 301 | + private String extractHealth(Map<AwsInstance<String>, TargetHealth> healths, AwsInstance<String> instance) { |
|
| 302 | + final String NO_HEALTH_VALUE_FOUND_MSG = "No health value found"; |
|
| 303 | + final String health_message; |
|
| 304 | + if (healths == null) { |
|
| 305 | + health_message = NO_HEALTH_VALUE_FOUND_MSG; |
|
| 306 | + } else { |
|
| 307 | + TargetHealth health = healths.get(instance); |
|
| 308 | + if (health != null) { |
|
| 309 | + health_message = health.state().toString(); |
|
| 310 | + } else { |
|
| 311 | + health_message = NO_HEALTH_VALUE_FOUND_MSG; |
|
| 312 | + } |
|
| 313 | + } |
|
| 314 | + return health_message; |
|
| 315 | + } |
|
| 316 | + |
|
| 317 | + @Override |
|
| 318 | + public void removeReverseProxy(ReverseProxyDTO proxy, String region, String optionalKeyName, byte[] privateKeyEncryptionPassphrase) |
|
| 319 | + throws Exception { |
|
| 320 | + checkLandscapeManageAwsPermission(); |
|
| 321 | + AwsRegion awsRegion = new AwsRegion(region, getLandscape()); |
|
| 322 | + AwsInstance<String> awsInstance = getLandscape().getHostByInstanceId(awsRegion, proxy.getInstanceId(), |
|
| 323 | + AwsInstanceImpl::new); |
|
| 324 | + getLandscape().getReverseProxyCluster(awsRegion).removeHost(awsInstance, Optional.of(optionalKeyName), privateKeyEncryptionPassphrase); |
|
| 325 | + } |
|
| 326 | + |
|
| 327 | + @Override |
|
| 328 | + public void addReverseProxy(String instanceName, String instanceType, String region, String launchKey, AvailabilityZoneDTO availabilityZoneDTO) { |
|
| 329 | + try { |
|
| 330 | + getLandscape().getReverseProxyCluster(new AwsRegion(region, getLandscape())).createHost(instanceName, |
|
| 331 | + InstanceType.valueOf(instanceType), |
|
| 332 | + new AwsAvailabilityZoneImpl(availabilityZoneDTO.getAzId(), availabilityZoneDTO.getAzName(), new AwsRegion(region, getLandscape())), launchKey); |
|
| 333 | + } catch (Exception e) { |
|
| 334 | + logger.log(Level.WARNING, e.getMessage()); |
|
| 335 | + } |
|
| 336 | + |
|
| 337 | + } |
|
| 338 | + |
|
| 233 | 339 | private MongoEndpoint getMongoEndpoint(MongoEndpointDTO mongoEndpointDTO) { |
| 234 | 340 | final MongoEndpoint result; |
| 235 | 341 | if (mongoEndpointDTO == null) { |
| ... | ... | @@ -258,10 +364,9 @@ public class LandscapeManagementWriteServiceImpl extends ResultCachingProxiedRem |
| 258 | 364 | } |
| 259 | 365 | |
| 260 | 366 | private AwsInstanceDTO convertToAwsInstanceDTO(Host host) { |
| 261 | - return new AwsInstanceDTO(host.getId().toString(), host.getAvailabilityZone().getId(), |
|
| 262 | - host.getPrivateAddress().getHostAddress(), |
|
| 263 | - host.getPublicAddress() == null ? null : host.getPublicAddress().getHostAddress(), |
|
| 264 | - host.getRegion().getId(), host.getLaunchTimePoint(), host.isSharedHost()); |
|
| 367 | + return new AwsInstanceDTO(host.getId().toString(), host.getPrivateAddress().getHostAddress(), host.getPublicAddress() == null ? null : host.getPublicAddress().getHostAddress(), |
|
| 368 | + host.getRegion().getId(), |
|
| 369 | + host.getLaunchTimePoint(), host.isSharedHost(), new AvailabilityZoneDTO(host.getAvailabilityZone().getName(), host.getRegion().getId(), host.getAvailabilityZone().getId())); |
|
| 265 | 370 | } |
| 266 | 371 | |
| 267 | 372 | @Override |
| ... | ... | @@ -475,7 +580,7 @@ public class LandscapeManagementWriteServiceImpl extends ResultCachingProxiedRem |
| 475 | 580 | final AwsRegion region = new AwsRegion(processToShutdown.getHost().getRegion(), landscape); |
| 476 | 581 | final AwsInstance<String> instance = new AwsInstanceImpl<>(processToShutdown.getHost().getInstanceId(), |
| 477 | 582 | new AwsAvailabilityZoneImpl(processToShutdown.getHost().getAvailabilityZoneId(), |
| 478 | - processToShutdown.getHost().getAvailabilityZoneId(), region), |
|
| 583 | + processToShutdown.getHost().getAvailabilityZoneName(), region), |
|
| 479 | 584 | InetAddress.getByName(processToShutdown.getHost().getPrivateIpAddress()), |
| 480 | 585 | processToShutdown.getHost().getLaunchTimePoint(), landscape); |
| 481 | 586 | instance.terminate(); |
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/shared/AvailabilityZoneDTO.java
| ... | ... | @@ -0,0 +1,29 @@ |
| 1 | +package com.sap.sailing.landscape.ui.shared; |
|
| 2 | + |
|
| 3 | +import com.google.gwt.user.client.rpc.IsSerializable; |
|
| 4 | + |
|
| 5 | +public class AvailabilityZoneDTO implements IsSerializable { |
|
| 6 | + private String azName; |
|
| 7 | + private String region; |
|
| 8 | + private String azId; |
|
| 9 | + @Deprecated |
|
| 10 | + AvailabilityZoneDTO() {} // GWT serialisation only |
|
| 11 | + |
|
| 12 | + public AvailabilityZoneDTO(String azName, String region, String azId) { |
|
| 13 | + this.azName = azName; |
|
| 14 | + this.region = region; |
|
| 15 | + this.azId = azId; |
|
| 16 | + } |
|
| 17 | + |
|
| 18 | + public String getAzName() { |
|
| 19 | + return azName; |
|
| 20 | + } |
|
| 21 | + |
|
| 22 | + public String getRegion() { |
|
| 23 | + return region; |
|
| 24 | + } |
|
| 25 | + |
|
| 26 | + public String getAzId() { |
|
| 27 | + return azId; |
|
| 28 | + } |
|
| 29 | +} |
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/shared/AwsInstanceDTO.java
| ... | ... | @@ -5,31 +5,37 @@ import com.sap.sse.common.TimePoint; |
| 5 | 5 | |
| 6 | 6 | public class AwsInstanceDTO implements IsSerializable { |
| 7 | 7 | private String instanceId; |
| 8 | - private String availabilityZoneId; |
|
| 8 | + private AvailabilityZoneDTO availabilityZoneDTO; |
|
| 9 | 9 | private String privateIpAddress; |
| 10 | 10 | private String publicIpAddress; |
| 11 | 11 | private String region; |
| 12 | 12 | private TimePoint launchTimePoint; |
| 13 | 13 | private boolean shared; |
| 14 | - |
|
| 15 | 14 | @Deprecated |
| 16 | 15 | AwsInstanceDTO() {} // for GWT RPC serialization only |
| 17 | 16 | |
| 18 | - public AwsInstanceDTO(String instanceId, String availabilityZoneId, String privateIpAddress, String publicIpAddress, String region, TimePoint launchTimePoint, boolean shared) { |
|
| 17 | + public AwsInstanceDTO(String instanceId, String privateIpAddress, String publicIpAddress, String region, TimePoint launchTimePoint, boolean shared, AvailabilityZoneDTO azDTO) { |
|
| 19 | 18 | super(); |
| 20 | 19 | this.instanceId = instanceId; |
| 21 | - this.availabilityZoneId = availabilityZoneId; |
|
| 20 | + this.availabilityZoneDTO = azDTO; |
|
| 22 | 21 | this.privateIpAddress = privateIpAddress; |
| 23 | 22 | this.publicIpAddress = publicIpAddress; |
| 24 | 23 | this.region = region; |
| 25 | 24 | this.launchTimePoint = launchTimePoint; |
| 26 | 25 | this.shared = shared; |
| 27 | 26 | } |
| 27 | + public String getAvailabilityZoneId() { |
|
| 28 | + return availabilityZoneDTO.getAzId(); |
|
| 29 | + } |
|
| 28 | 30 | public String getInstanceId() { |
| 29 | 31 | return instanceId; |
| 30 | 32 | } |
| 31 | - public String getAvailabilityZoneId() { |
|
| 32 | - return availabilityZoneId; |
|
| 33 | + |
|
| 34 | + public AvailabilityZoneDTO getAvailabilityZoneDTO() { |
|
| 35 | + return availabilityZoneDTO; |
|
| 36 | + } |
|
| 37 | + public String getAvailabilityZoneName() { |
|
| 38 | + return availabilityZoneDTO.getAzName(); |
|
| 33 | 39 | } |
| 34 | 40 | public String getRegion() { |
| 35 | 41 | return region; |
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/shared/ReverseProxyDTO.java
| ... | ... | @@ -0,0 +1,42 @@ |
| 1 | +package com.sap.sailing.landscape.ui.shared; |
|
| 2 | + |
|
| 3 | +import com.sap.sse.common.Named; |
|
| 4 | +import com.sap.sse.common.TimePoint; |
|
| 5 | + |
|
| 6 | +public class ReverseProxyDTO extends AwsInstanceDTO implements Named { |
|
| 7 | + private static final long serialVersionUID = 1576177375197043472L; |
|
| 8 | + |
|
| 9 | + @Deprecated |
|
| 10 | + ReverseProxyDTO() {} |
|
| 11 | + |
|
| 12 | + private String name; |
|
| 13 | + private String amiId; |
|
| 14 | + private String health; |
|
| 15 | + private boolean isDisposable = false; |
|
| 16 | + |
|
| 17 | + public ReverseProxyDTO(String instanceId, String privateIpAddress, String publicIpAddress, |
|
| 18 | + String region, TimePoint launchTimePoint, boolean shared, String name, String imageId, String healthInTargetGroup, |
|
| 19 | + boolean isDisposable, AvailabilityZoneDTO availabilityZoneDTO) { |
|
| 20 | + super(instanceId, privateIpAddress, publicIpAddress, region, launchTimePoint, shared, availabilityZoneDTO); |
|
| 21 | + this.name = name; |
|
| 22 | + this.amiId = imageId; |
|
| 23 | + this.health = healthInTargetGroup; |
|
| 24 | + this.isDisposable = isDisposable; |
|
| 25 | + } |
|
| 26 | + |
|
| 27 | + public String getName() { |
|
| 28 | + return name; |
|
| 29 | + } |
|
| 30 | + |
|
| 31 | + public String getImageId() { |
|
| 32 | + return amiId; |
|
| 33 | + } |
|
| 34 | + |
|
| 35 | + public String getHealth() { |
|
| 36 | + return health; |
|
| 37 | + } |
|
| 38 | + |
|
| 39 | + public boolean isDisposable() { |
|
| 40 | + return isDisposable; |
|
| 41 | + } |
|
| 42 | +} |
java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/impl/LandscapeServiceImpl.java
| ... | ... | @@ -479,13 +479,13 @@ public class LandscapeServiceImpl implements LandscapeService { |
| 479 | 479 | for (final Iterable<UUID> eids : Util.map(mdiResult.getLeaderboardGroupsImported(), lgWithEventIds->lgWithEventIds.getEventIds())) { |
| 480 | 480 | Util.addAll(eids, eventIDs); |
| 481 | 481 | } |
| 482 | - final ReverseProxy<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>, RotatingFileBasedLog> centralReverseProxy = |
|
| 483 | - getLandscape().getCentralReverseProxy(region); |
|
| 482 | + final ReverseProxy<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>, RotatingFileBasedLog> reverseProxyCluster = |
|
| 483 | + getLandscape().getReverseProxyCluster(region); |
|
| 484 | 484 | // TODO bug5311: when refactoring this for general scope migration, moving to a dedicated replica set will not require this |
| 485 | 485 | // TODO bug5311: when refactoring this for general scope migration, moving into a cold storage server other than ARCHIVE will require ALBToReverseProxyRedirectMapper instead |
| 486 | 486 | logger.info("Adding reverse proxy rules for migrated content pointing to ARCHIVE"); |
| 487 | 487 | defaultRedirect.accept(new ALBToReverseProxyArchiveRedirectMapper<>( |
| 488 | - centralReverseProxy, hostnameFromWhichToArchive, Optional.ofNullable(optionalKeyName), passphraseForPrivateKeyDecryption)); |
|
| 488 | + reverseProxyCluster, hostnameFromWhichToArchive, Optional.ofNullable(optionalKeyName), passphraseForPrivateKeyDecryption)); |
|
| 489 | 489 | if (removeApplicationReplicaSet) { |
| 490 | 490 | logger.info("Removing remote sailing server references to "+from+" from archive server "+archive); |
| 491 | 491 | try { |
java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/procedures/StartMultiServer.java
| ... | ... | @@ -8,7 +8,8 @@ import com.sap.sse.common.Duration; |
| 8 | 8 | import com.sap.sse.landscape.aws.HostSupplier; |
| 9 | 9 | import com.sap.sse.landscape.aws.Tags; |
| 10 | 10 | import com.sap.sse.landscape.aws.orchestration.StartEmptyServer; |
| 11 | - |
|
| 11 | +import com.sap.sse.shared.util.Wait; |
|
| 12 | +import software.amazon.awssdk.services.ec2.model.InstanceStateName; |
|
| 12 | 13 | import software.amazon.awssdk.services.ec2.model.InstanceType; |
| 13 | 14 | |
| 14 | 15 | /** |
| ... | ... | @@ -125,6 +126,6 @@ implements StartFromSailingAnalyticsImage { |
| 125 | 126 | @Override |
| 126 | 127 | public void run() throws Exception { |
| 127 | 128 | super.run(); // this will trigger the "sailing" init.d script running in the background |
| 128 | - getHost().getReverseProxy().createInternalStatusRedirect(optionalTimeout, Optional.of(getKeyName()), getPrivateKeyEncryptionPassphrase()); |
|
| 129 | + Wait.wait(() -> getHost().getInstance().state().name().equals(InstanceStateName.RUNNING), optionalTimeout, Duration.ONE_SECOND.times(10)); |
|
| 129 | 130 | } |
| 130 | 131 | } |
java/com.sap.sailing.server.gateway/webservices/api/v1/gpsFixesPostDoc.html
| ... | ... | @@ -82,7 +82,7 @@ These can be mapped to a competitor or a mark via the DeviceMappings in the Race |
| 82 | 82 | "latitude" : 55.12456, |
| 83 | 83 | "longitude" : 8.03456, |
| 84 | 84 | "speed" : 5.1, |
| 85 | - "course" : 14.2 |
|
| 85 | + "course" : 14.2, |
|
| 86 | 86 | "hdt" : 14.7 |
| 87 | 87 | } |
| 88 | 88 | ] |
java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/dialog/DataEntryDialog.java
| ... | ... | @@ -95,7 +95,7 @@ public abstract class DataEntryDialog<T> { |
| 95 | 95 | |
| 96 | 96 | /** |
| 97 | 97 | * @param message |
| 98 | - * may be {@code null} |
|
| 98 | + * Displayed beneath the title. May be {@code null} |
|
| 99 | 99 | * @param cancelButtonName |
| 100 | 100 | * if {@code null}, no cancel button will be displayed |
| 101 | 101 | * @param validator |
java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/AwsLandscape.java
| ... | ... | @@ -5,7 +5,6 @@ import java.util.Optional; |
| 5 | 5 | import java.util.concurrent.CompletableFuture; |
| 6 | 6 | import java.util.concurrent.ExecutionException; |
| 7 | 7 | import java.util.concurrent.TimeoutException; |
| 8 | - |
|
| 9 | 8 | import com.jcraft.jsch.JSchException; |
| 10 | 9 | import com.jcraft.jsch.KeyPair; |
| 11 | 10 | import com.sap.sse.common.Duration; |
| ... | ... | @@ -35,7 +34,6 @@ import com.sap.sse.landscape.mongodb.MongoProcessInReplicaSet; |
| 35 | 34 | import com.sap.sse.landscape.mongodb.MongoReplicaSet; |
| 36 | 35 | import com.sap.sse.landscape.mongodb.impl.MongoProcessImpl; |
| 37 | 36 | import com.sap.sse.landscape.ssh.SSHKeyPair; |
| 38 | - |
|
| 39 | 37 | import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; |
| 40 | 38 | import software.amazon.awssdk.auth.credentials.AwsCredentials; |
| 41 | 39 | import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; |
| ... | ... | @@ -128,8 +126,6 @@ public interface AwsLandscape<ShardingKey> extends Landscape<ShardingKey> { |
| 128 | 126 | |
| 129 | 127 | String MONGO_REPLICA_SET_NAME_AND_PORT_SEPARATOR = ":"; |
| 130 | 128 | |
| 131 | - String CENTRAL_REVERSE_PROXY_TAG_NAME = "CentralReverseProxy"; |
|
| 132 | - |
|
| 133 | 129 | /** |
| 134 | 130 | * Based on system properties for the AWS access key ID and the secret access key (see |
| 135 | 131 | * {@link #ACCESS_KEY_ID_SYSTEM_PROPERTY_NAME} and {@link #SECRET_ACCESS_KEY_SYSTEM_PROPERTY_NAME}), this method |
| ... | ... | @@ -581,10 +577,17 @@ public interface AwsLandscape<ShardingKey> extends Landscape<ShardingKey> { |
| 581 | 577 | |
| 582 | 578 | // --------------- abstract landscape view -------------- |
| 583 | 579 | /** |
| 584 | - * Obtains the reverse proxy in the given {@code region} that is used to receive (and possibly redirect to HTTPS or |
|
| 580 | + * Obtains the reverse proxies, which make up a reverse proxy cluster, in the given {@code region} that are used to receive (and possibly redirect to HTTPS or |
|
| 585 | 581 | * forward to a host proxied by the reverse proxy) all HTTP requests and any HTTPS request not handled by a |
| 586 | 582 | * dedicated load balancer rule, such as "cold storage" hostnames that have been archived. May return {@code null} |
| 587 | - * in case in the given {@code region} no such reverse proxy has been configured / set up yet. |
|
| 583 | + * if, in the given {@code region}, no such reverse proxy has been configured / set up yet. |
|
| 584 | + */ |
|
| 585 | + <MetricsT extends ApplicationProcessMetrics, ProcessT extends AwsApplicationProcess<ShardingKey, MetricsT, ProcessT>> |
|
| 586 | + ReverseProxyCluster<ShardingKey, MetricsT, ProcessT, RotatingFileBasedLog> getReverseProxyCluster(Region region); |
|
| 587 | + |
|
| 588 | + /** |
|
| 589 | + * Returns the reverse proxy in the given region, but encapsulated within a ReverseProxyCluster. |
|
| 590 | + * @return |
|
| 588 | 591 | */ |
| 589 | 592 | <MetricsT extends ApplicationProcessMetrics, ProcessT extends AwsApplicationProcess<ShardingKey, MetricsT, ProcessT>> |
| 590 | 593 | ReverseProxyCluster<ShardingKey, MetricsT, ProcessT, RotatingFileBasedLog> getCentralReverseProxy(Region region); |
| ... | ... | @@ -861,4 +864,14 @@ public interface AwsLandscape<ShardingKey> extends Landscape<ShardingKey> { |
| 861 | 864 | * it is used to filter for only those AZs that have a subnet configured in the VPC. |
| 862 | 865 | */ |
| 863 | 866 | Iterable<AwsAvailabilityZone> getAvailabilityZones(com.sap.sse.landscape.Region region, Optional<String> vpcId); |
| 867 | + |
|
| 868 | + /** |
|
| 869 | + * Adds hosts to an IP-based target group. |
|
| 870 | + */ |
|
| 871 | + void addIpTargetToTargetGroup(TargetGroup<ShardingKey> targetGroup, Iterable<AwsInstance<ShardingKey>> hosts); |
|
| 872 | + |
|
| 873 | + /** |
|
| 874 | + * Removes hosts from an IP-based target group. |
|
| 875 | + */ |
|
| 876 | + void removeIpTargetFromTargetGroup(TargetGroup<ShardingKey> targetGroup, Iterable<AwsInstance<ShardingKey>> hosts); |
|
| 864 | 877 | } |
java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/LandscapeConstants.java
| ... | ... | @@ -0,0 +1,61 @@ |
| 1 | +package com.sap.sse.landscape.aws; |
|
| 2 | + |
|
| 3 | +public interface LandscapeConstants { |
|
| 4 | + /** |
|
| 5 | + * The key <strong>tag</strong>, indicating that an instance only acts as a reverse proxy |
|
| 6 | + * (ie. it is not hosting other services). SO it can be terminated without risk. |
|
| 7 | + */ |
|
| 8 | + String DISPOSABLE_PROXY = "DisposableProxy"; |
|
| 9 | + |
|
| 10 | + /** |
|
| 11 | + * The tag name for a target group, which contains all the instances running httpd. Used to mark the |
|
| 12 | + * target groups containing all of the reverse proxies, disposable and otherwise. This allows for health checks |
|
| 13 | + * to be accessed and marks which groups any new reverse proxy instances should be added to. |
|
| 14 | + */ |
|
| 15 | + String ALL_REVERSE_PROXIES = "allReverseProxies"; |
|
| 16 | + |
|
| 17 | + /** |
|
| 18 | + * The tag name for a target group containing only the central reverse proxy. |
|
| 19 | + */ |
|
| 20 | + String JUST_CENTRAL_REVERSE_PROXY = "CentralReverseProxy"; |
|
| 21 | + |
|
| 22 | + /** |
|
| 23 | + * A string that all network load balancers contain. |
|
| 24 | + */ |
|
| 25 | + String NLB_ARN_CONTAINS = "loadbalancer/net"; |
|
| 26 | + |
|
| 27 | + /** |
|
| 28 | + * The value of "image-type" tag, which is used to identify which image to use to deploy a disposable reverse proxy. |
|
| 29 | + */ |
|
| 30 | + String IMAGE_TYPE_REVERSE_PROXY = "disposable-reverse-proxy"; |
|
| 31 | + |
|
| 32 | + /** |
|
| 33 | + * Indicates an instance is a reverse proxy. |
|
| 34 | + */ |
|
| 35 | + String REVERSE_PROXY_TAG_NAME = "ReverseProxy"; |
|
| 36 | + |
|
| 37 | + /** |
|
| 38 | + * The tag for a security group that a reverse proxy requires. |
|
| 39 | + */ |
|
| 40 | + String REVERSE_PROXY_SG_TAG = "reverse-proxy-sg"; |
|
| 41 | + |
|
| 42 | + /** |
|
| 43 | + * The tag for any security groups for a mongo DB. |
|
| 44 | + */ |
|
| 45 | + String MONGO_SG_TAG = "mongo-sg"; |
|
| 46 | + |
|
| 47 | + /** |
|
| 48 | + * The tag for a security group for an application server. |
|
| 49 | + */ |
|
| 50 | + String SAILING_APPLICATION_SG_TAG = "application-server-sg"; |
|
| 51 | + |
|
| 52 | + /** |
|
| 53 | + * Indicates that an instance is suitable for co-deployment with httpd. |
|
| 54 | + */ |
|
| 55 | + String INSTANCE_SUITABLE_FOR_HTTPD = "canCoDeployWithHttpd"; |
|
| 56 | + |
|
| 57 | + /** |
|
| 58 | + * The tag for the central reverse proxy, which also hosts non-essential services. |
|
| 59 | + */ |
|
| 60 | + String CENTRAL_REVERSE_PROXY_TAG_NAME = "CentralReverseProxy"; |
|
| 61 | +} |
java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/ReverseProxy.java
| ... | ... | @@ -2,8 +2,6 @@ package com.sap.sse.landscape.aws; |
| 2 | 2 | |
| 3 | 3 | import java.util.Optional; |
| 4 | 4 | import java.util.UUID; |
| 5 | - |
|
| 6 | -import com.sap.sse.common.Duration; |
|
| 7 | 5 | import com.sap.sse.landscape.Host; |
| 8 | 6 | import com.sap.sse.landscape.Log; |
| 9 | 7 | import com.sap.sse.landscape.application.ApplicationProcess; |
| ... | ... | @@ -99,11 +97,6 @@ public interface ReverseProxy<ShardingKey, MetricsT extends ApplicationProcessMe |
| 99 | 97 | void setScopeRedirect(Scope<ShardingKey> scope, ProcessT applicationProcess) throws Exception; |
| 100 | 98 | |
| 101 | 99 | /** |
| 102 | - * Creates a mapping for the {@code /internal-server-status} path using the host's generic external ec2 host name |
|
| 103 | - */ |
|
| 104 | - void createInternalStatusRedirect(Optional<Duration> optionalTimeout, Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception; |
|
| 105 | - |
|
| 106 | - /** |
|
| 107 | 100 | * Removes any existing redirect mapping for the {@code hostname} provided. If no such mapping |
| 108 | 101 | * exists, the method does nothing. |
| 109 | 102 | */ |
java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/ReverseProxyCluster.java
| ... | ... | @@ -1,9 +1,10 @@ |
| 1 | 1 | package com.sap.sse.landscape.aws; |
| 2 | 2 | |
| 3 | +import java.util.Optional; |
|
| 4 | +import java.util.concurrent.TimeoutException; |
|
| 3 | 5 | import com.sap.sse.landscape.Log; |
| 4 | 6 | import com.sap.sse.landscape.application.ApplicationProcess; |
| 5 | 7 | import com.sap.sse.landscape.application.ApplicationProcessMetrics; |
| 6 | - |
|
| 7 | 8 | import software.amazon.awssdk.services.ec2.model.InstanceType; |
| 8 | 9 | |
| 9 | 10 | /** |
| ... | ... | @@ -16,27 +17,36 @@ import software.amazon.awssdk.services.ec2.model.InstanceType; |
| 16 | 17 | * |
| 17 | 18 | */ |
| 18 | 19 | public interface ReverseProxyCluster<ShardingKey, MetricsT extends ApplicationProcessMetrics, ProcessT extends ApplicationProcess<ShardingKey, MetricsT, ProcessT>, LogT extends Log> |
| 19 | -extends ReverseProxy<ShardingKey, MetricsT, ProcessT, LogT> { |
|
| 20 | + extends ReverseProxy<ShardingKey, MetricsT, ProcessT, LogT> { |
|
| 20 | 21 | /** |
| 21 | 22 | * A reverse proxy may scale out by adding more hosts. |
| 22 | 23 | * |
| 23 | 24 | * @return at least one host |
| 24 | 25 | */ |
| 25 | 26 | Iterable<AwsInstance<ShardingKey>> getHosts(); |
| 26 | - |
|
| 27 | + |
|
| 27 | 28 | /** |
| 28 | - * Add one host of the instance type specified to the availability zone {@code az}. |
|
| 29 | + * Add one host of the instance type specified to the availability zone {@code az}. Additionally, it is added to the |
|
| 30 | + * target groups with {@link LandscapeConstants#ALL_REVERSE_PROXIES}, once it is running, which may be the reason |
|
| 31 | + * the success message doesn't immediately appear. |
|
| 29 | 32 | * |
| 30 | 33 | * @return the host that was added by this request; it will also be part of the response of {@link #getHosts()} now |
| 31 | 34 | */ |
| 32 | - AwsInstance<ShardingKey> createHost(InstanceType instanceType, AwsAvailabilityZone az, String keyName); |
|
| 33 | - |
|
| 35 | + AwsInstance<ShardingKey> createHost(String name, InstanceType instanceType, AwsAvailabilityZone az, String keyName) |
|
| 36 | + throws TimeoutException, Exception; |
|
| 37 | + |
|
| 38 | + /** |
|
| 39 | + * Adds the existing passed host to the cluster. The cluster though lacks longevity because any changes to the host are not reflected in the cluster. |
|
| 40 | + */ |
|
| 34 | 41 | void addHost(AwsInstance<ShardingKey> host); |
| 35 | 42 | |
| 36 | 43 | /** |
| 37 | 44 | * Removes a single host from this reverse proxy, terminating the host. When trying to remove the last remaining |
| 38 | 45 | * host, an {@link IllegalStateException} will be thrown and the method will not complete the request. Consider |
| 39 | - * using {@link #terminate()} to terminate all hosts forming this reverse proxy. |
|
| 46 | + * using {@link #terminate()} to terminate all hosts forming this reverse proxy. The instance is |
|
| 47 | + * <strong>only</strong> terminated when no longer in the draining state in a target group (typically takes 5 |
|
| 48 | + * minutes, so expect a delay). |
|
| 40 | 49 | */ |
| 41 | - void removeHost(AwsInstance<ShardingKey> host); |
|
| 50 | + void removeHost(AwsInstance<ShardingKey> host, Optional<String> optionalKeyName, |
|
| 51 | + byte[] privateKeyEncryptionPassphrase) throws Exception; |
|
| 42 | 52 | } |
java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AbstractApacheReverseProxy.java
| ... | ... | @@ -9,7 +9,10 @@ import com.sap.sse.landscape.aws.ReverseProxy; |
| 9 | 9 | public abstract class AbstractApacheReverseProxy<ShardingKey, MetricsT extends ApplicationProcessMetrics, |
| 10 | 10 | ProcessT extends ApplicationProcess<ShardingKey, MetricsT, ProcessT>> |
| 11 | 11 | implements ReverseProxy<ShardingKey, MetricsT, ProcessT, RotatingFileBasedLog> { |
| 12 | - protected static final String INTERNAL_SERVER_STATUS = "internal-server-status"; |
|
| 12 | + /** |
|
| 13 | + * The path the target group will ping to check the health of the reverse proxy. |
|
| 14 | + */ |
|
| 15 | + protected static final String INTERNAL_SERVER_STATUS = "cgi-bin/reverseProxyHealthcheck.sh"; |
|
| 13 | 16 | private final AwsLandscape<ShardingKey> landscape; |
| 14 | 17 | |
| 15 | 18 | public AbstractApacheReverseProxy(AwsLandscape<ShardingKey> landscape) { |
java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/ApacheReverseProxy.java
| ... | ... | @@ -6,7 +6,6 @@ import java.util.Optional; |
| 6 | 6 | import java.util.UUID; |
| 7 | 7 | import java.util.logging.Level; |
| 8 | 8 | import java.util.logging.Logger; |
| 9 | - |
|
| 10 | 9 | import com.sap.sse.common.Duration; |
| 11 | 10 | import com.sap.sse.landscape.Host; |
| 12 | 11 | import com.sap.sse.landscape.RotatingFileBasedLog; |
| ... | ... | @@ -45,17 +44,26 @@ implements com.sap.sse.landscape.Process<RotatingFileBasedLog, MetricsT> { |
| 45 | 44 | private static final Optional<Duration> TIMEOUT = Optional.of(Duration.ONE_MINUTE.times(5)); |
| 46 | 45 | |
| 47 | 46 | /** |
| 48 | - * The configuration directory where files with extension {@link #CONFIG_FILE_EXTENSION} can be placed which |
|
| 47 | + * The configuration directory within the "httpd/config" git repo where files with extension {@link #CONFIG_FILE_EXTENSION} can be placed which |
|
| 49 | 48 | * a {@code reload} will pick up and evaluate. |
| 50 | 49 | */ |
| 51 | - private static final String CONFIG_PATH = "/etc/httpd/conf.d"; |
|
| 50 | + private static final String RELATIVE_CONFIG_PATH = "conf.d"; |
|
| 51 | + |
|
| 52 | + /** |
|
| 53 | + * The absolute path to the "httpd config" git repo which stores all httpd configuration files. |
|
| 54 | + */ |
|
| 55 | + private static final String CONFIG_REPO_PATH = "/etc/httpd"; |
|
| 56 | + |
|
| 57 | + /** |
|
| 58 | + * The branch name that contains the production httpd configuration. |
|
| 59 | + */ |
|
| 60 | + private static final String CONFIG_REPO_MAIN_BRANCH_NAME = "main"; |
|
| 52 | 61 | |
| 53 | 62 | /** |
| 54 | 63 | * Extension for files in the {@link #CONFIG_PATH} folder that will automatically be picked up when reloading |
| 55 | 64 | * the proxy's configuration. |
| 56 | 65 | */ |
| 57 | 66 | private static final String CONFIG_FILE_EXTENSION = ".conf"; |
| 58 | - |
|
| 59 | 67 | private static final String HOME_REDIRECT_MACRO = "Home-SSL"; |
| 60 | 68 | private static final String PLAIN_REDIRECT_MACRO = "Plain-SSL"; |
| 61 | 69 | private static final String EVENT_REDIRECT_MACRO = "Event-SSL"; |
| ... | ... | @@ -63,9 +71,6 @@ implements com.sap.sse.landscape.Process<RotatingFileBasedLog, MetricsT> { |
| 63 | 71 | private static final String HOME_ARCHIVE_REDIRECT_MACRO = "Home-ARCHIVE"; |
| 64 | 72 | private static final String EVENT_ARCHIVE_REDIRECT_MACRO = "Event-ARCHIVE"; |
| 65 | 73 | private static final String SERIES_ARCHIVE_REDIRECT_MACRO = "Series-ARCHIVE"; |
| 66 | - private static final String STATUS = "Status"; |
|
| 67 | - private static final String CONFIG_FILE_FOR_INTERNALS = "001-internals"+CONFIG_FILE_EXTENSION; |
|
| 68 | - |
|
| 69 | 74 | private final AwsInstance<ShardingKey> host; |
| 70 | 75 | |
| 71 | 76 | public ApacheReverseProxy(AwsLandscape<ShardingKey> landscape, AwsInstance<ShardingKey> host) { |
| ... | ... | @@ -74,18 +79,57 @@ implements com.sap.sse.landscape.Process<RotatingFileBasedLog, MetricsT> { |
| 74 | 79 | } |
| 75 | 80 | |
| 76 | 81 | private String getConfigFileNameForScope(Scope<ShardingKey> scope) { |
| 77 | - return scope.toString()+CONFIG_FILE_EXTENSION; |
|
| 82 | + return scope.toString() + CONFIG_FILE_EXTENSION; |
|
| 78 | 83 | } |
| 79 | 84 | |
| 85 | + /** |
|
| 86 | + * Appends the {@link #CONFIG_FILE_EXTENSION} to the hostname. |
|
| 87 | + */ |
|
| 80 | 88 | private String getConfigFileNameForHostname(String hostname) { |
| 81 | - return hostname+CONFIG_FILE_EXTENSION; |
|
| 89 | + return hostname + CONFIG_FILE_EXTENSION; |
|
| 82 | 90 | } |
| 83 | - |
|
| 91 | + |
|
| 92 | + /** |
|
| 93 | + * Forces a logrotate on the instance and logs the output. |
|
| 94 | + * @param optionalKeyName The optional key to use for the ssh connection. |
|
| 95 | + * @param privateKeyEncryptionPassphrase The password for the key. |
|
| 96 | + */ |
|
| 97 | + public void rotateLogs(Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception { |
|
| 98 | + final String command = "logrotate --force -v /etc/logrotate.d/httpd 2>&1; echo \"logrotate done\""; |
|
| 99 | + logger.info("Standard output from forced log rotate on " + this.getHostname() + ": " + runCommandAndReturnStdoutAndStderr(command, "Standard error from logrotate ", |
|
| 100 | + Level.ALL, optionalKeyName, privateKeyEncryptionPassphrase)); |
|
| 101 | + } |
|
| 102 | + |
|
| 103 | + /** |
|
| 104 | + * Creates a redirect file and updates the git repo. |
|
| 105 | + * |
|
| 106 | + * @param configFileNameForHostname |
|
| 107 | + * The config file to create or edit, with the appropriate extension appended (eg. .conf). |
|
| 108 | + * @param macroName |
|
| 109 | + * The name of the macro to use. |
|
| 110 | + * @param hostname |
|
| 111 | + * The hostname the macro will affect. Typically the same as the file name. |
|
| 112 | + * @param optionalKeyName |
|
| 113 | + * Key name to use for the ssh channel. |
|
| 114 | + * @param privateKeyEncryptionPassphrase |
|
| 115 | + * The passphrase for the passed key. |
|
| 116 | + * @param doCommit |
|
| 117 | + * Boolean indicating whether to commit. True if the changed file should be committed. |
|
| 118 | + * @param doPush |
|
| 119 | + * Boolean indicating whether to push the committed changes. True if the commit should be pushed. |
|
| 120 | + * @param macroArguments |
|
| 121 | + * Optional macro arguments. |
|
| 122 | + */ |
|
| 84 | 123 | private void setRedirect(String configFileNameForHostname, String macroName, String hostname, |
| 85 | - Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase, String... macroArguments) |
|
| 124 | + Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase, boolean doCommit, boolean doPush, String... macroArguments) |
|
| 86 | 125 | throws Exception { |
| 87 | - final String command = "echo \"Use " + macroName + " " + hostname + " " + String.join(" ", macroArguments) |
|
| 88 | - + "\" >" + getConfigFilePath(configFileNameForHostname) + "; service httpd reload"; |
|
| 126 | + String command = "echo \"Use " + macroName + " " + hostname + " " + String.join(" ", macroArguments) |
|
| 127 | + + "\" > " + getAbsoluteConfigFilePath(configFileNameForHostname) + "; service httpd reload" ; |
|
| 128 | + if (doCommit) { |
|
| 129 | + command = command + " && cd " |
|
| 130 | + + CONFIG_REPO_PATH + " && " + createCommitAndPushString(configFileNameForHostname, |
|
| 131 | + "Set " + configFileNameForHostname + " redirect", doPush); |
|
| 132 | + } |
|
| 89 | 133 | logger.info("Standard output from setting up the re-direct for " + hostname |
| 90 | 134 | + " and reloading the Apache httpd server: " |
| 91 | 135 | + runCommandAndReturnStdoutAndStderr(command, |
| ... | ... | @@ -93,6 +137,18 @@ implements com.sap.sse.landscape.Process<RotatingFileBasedLog, MetricsT> { |
| 93 | 137 | + " and reloading the Apache httpd server: ", |
| 94 | 138 | Level.INFO, optionalKeyName, privateKeyEncryptionPassphrase)); |
| 95 | 139 | } |
| 140 | + |
|
| 141 | + /** |
|
| 142 | + * Overloads {@link #setRedirect(String, String, String, Optional, byte[], boolean, boolean, String...)} and defaults to true and true for committing and pushing. |
|
| 143 | + * |
|
| 144 | + * @see #setRedirect(String, String, String, Optional, byte[], boolean, boolean, String...) |
|
| 145 | + */ |
|
| 146 | + public void setRedirect(String configFileNameForHostname, String macroName, String hostname, |
|
| 147 | + Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase, String... macroArguments) |
|
| 148 | + throws Exception { |
|
| 149 | + setRedirect(configFileNameForHostname, macroName, hostname, optionalKeyName, privateKeyEncryptionPassphrase, |
|
| 150 | + true, true, macroArguments); |
|
| 151 | + } |
|
| 96 | 152 | |
| 97 | 153 | private String runCommandAndReturnStdoutAndStderr(String command, String stderrLogPrefix, Level stderrLogLevel, Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception { |
| 98 | 154 | final SshCommandChannel sshChannel = getHost().createRootSshChannel(TIMEOUT, optionalKeyName, privateKeyEncryptionPassphrase); |
| ... | ... | @@ -100,8 +156,40 @@ implements com.sap.sse.landscape.Process<RotatingFileBasedLog, MetricsT> { |
| 100 | 156 | return stdout; |
| 101 | 157 | } |
| 102 | 158 | |
| 103 | - private String getConfigFilePath(String configFileNameForHostname) { |
|
| 104 | - return CONFIG_PATH+"/"+configFileNameForHostname; |
|
| 159 | + /** |
|
| 160 | + * Creates a command, that can be ran on an instance to commit, and optionally push, changes to a file (within a git repository). ASSUMES the command is ran from within the repository. |
|
| 161 | + * @param editedFileName The file name edited, created or deleted to commit. This includes the {@link #CONFIG_FILE_EXTENSION}, but not a path. |
|
| 162 | + * @param commitMsg The commit message, without escaped speech marks. |
|
| 163 | + * @param performPush Boolean indicating whether to push changes or not. True for performing a push. |
|
| 164 | + * @return Returns the created command (in String form) to perform a commit and optional push. |
|
| 165 | + */ |
|
| 166 | + private String createCommitAndPushString(String editedFileName, String commitMsg, boolean performPush) { |
|
| 167 | + StringBuilder command = new StringBuilder(" git add " + getRelativeConfigFilePath(editedFileName) |
|
| 168 | + + " && git commit -m " + "\"" + commitMsg + "\""); // space at beginning and after -m, for safety |
|
| 169 | + if (performPush) { |
|
| 170 | + command.append(" ; GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no\" git push origin " + CONFIG_REPO_MAIN_BRANCH_NAME); |
|
| 171 | + } |
|
| 172 | + return command.toString(); |
|
| 173 | + } |
|
| 174 | + |
|
| 175 | + /** |
|
| 176 | + * |
|
| 177 | + * @param configFileNameForHostname |
|
| 178 | + * The name of the file to append to the relative path. |
|
| 179 | + * @return Returns the relative path. This is the path, within the directory specified by {@link #CONFIG_REPO_PATH}, |
|
| 180 | + * to where the argument file is or may be. |
|
| 181 | + */ |
|
| 182 | + private String getRelativeConfigFilePath(String configFileNameForHostname) { |
|
| 183 | + return RELATIVE_CONFIG_PATH + "/" + configFileNameForHostname; |
|
| 184 | + } |
|
| 185 | + |
|
| 186 | + /** |
|
| 187 | + * |
|
| 188 | + * @param configFileNameForHostname The filename to append to the absolute path. |
|
| 189 | + * @return Returns the absolute path to the config file passed as an argument (which may be for creation, deletion or just finding the file). |
|
| 190 | + */ |
|
| 191 | + private String getAbsoluteConfigFilePath(String configFileNameForHostname) { |
|
| 192 | + return CONFIG_REPO_PATH + "/" + RELATIVE_CONFIG_PATH + "/" + configFileNameForHostname; |
|
| 105 | 193 | } |
| 106 | 194 | |
| 107 | 195 | @Override |
| ... | ... | @@ -156,29 +244,29 @@ implements com.sap.sse.landscape.Process<RotatingFileBasedLog, MetricsT> { |
| 156 | 244 | } |
| 157 | 245 | |
| 158 | 246 | @Override |
| 159 | - public void createInternalStatusRedirect(Optional<Duration> optionalTimeout, Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception { |
|
| 160 | - setRedirect(CONFIG_FILE_FOR_INTERNALS, STATUS, getHost().getPrivateAddress(optionalTimeout).getHostAddress(), optionalKeyName, privateKeyEncryptionPassphrase, INTERNAL_SERVER_STATUS); |
|
| 161 | - } |
|
| 162 | - |
|
| 163 | - @Override |
|
| 164 | 247 | public void removeRedirect(Scope<ShardingKey> scope, Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception { |
| 165 | - final String configFilePath = getConfigFilePath(getConfigFileNameForScope(scope)); |
|
| 166 | - removeRedirect(configFilePath, scope.toString(), optionalKeyName, privateKeyEncryptionPassphrase); |
|
| 248 | + final String configFileName = getConfigFileNameForScope(scope); |
|
| 249 | + removeRedirect(configFileName, scope.toString(), optionalKeyName, privateKeyEncryptionPassphrase); |
|
| 167 | 250 | } |
| 168 | 251 | |
| 169 | 252 | @Override |
| 170 | 253 | public void removeRedirect(String hostname, Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception { |
| 171 | - final String configFilePath = getConfigFilePath(getConfigFileNameForHostname(hostname)); |
|
| 172 | - removeRedirect(configFilePath, hostname, optionalKeyName, privateKeyEncryptionPassphrase); |
|
| 254 | + final String configFileName = getConfigFileNameForHostname(hostname); |
|
| 255 | + removeRedirect(configFileName, hostname, optionalKeyName, privateKeyEncryptionPassphrase); |
|
| 173 | 256 | } |
| 174 | 257 | |
| 175 | - private void removeRedirect(String configFilePath, String redirectNameForLogOutput, |
|
| 258 | + |
|
| 259 | + /** |
|
| 260 | + * @param configFileName The name of the file to remove. |
|
| 261 | + * @param hostname The hostname which was removed. |
|
| 262 | + */ |
|
| 263 | + private void removeRedirect(String configFileName, String hostname, |
|
| 176 | 264 | Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception { |
| 177 | - final String command = "rm " + configFilePath + "; service httpd reload"; |
|
| 178 | - logger.info("Standard output from removing the re-direct for " + redirectNameForLogOutput |
|
| 265 | + final String command = "rm " + getAbsoluteConfigFilePath(configFileName) + "; service httpd reload" + " && cd " + CONFIG_REPO_PATH + ";" + createCommitAndPushString(configFileName, "Removed " + hostname, true); |
|
| 266 | + logger.info("Standard output from removing the re-direct for " + hostname |
|
| 179 | 267 | + " and reloading the Apache httpd server: " |
| 180 | 268 | + runCommandAndReturnStdoutAndStderr(command, |
| 181 | - "Standard error from removing the re-direct for " + redirectNameForLogOutput |
|
| 269 | + "Standard error from removing the re-direct for " + hostname |
|
| 182 | 270 | + " and reloading the Apache httpd server: ", |
| 183 | 271 | Level.INFO, optionalKeyName, privateKeyEncryptionPassphrase)); |
| 184 | 272 | } |
| ... | ... | @@ -190,7 +278,7 @@ implements com.sap.sse.landscape.Process<RotatingFileBasedLog, MetricsT> { |
| 190 | 278 | |
| 191 | 279 | @Override |
| 192 | 280 | public int getPort() { |
| 193 | - return 443; // TODO currently, we offload SSL only at the reverse proxies; but we should change this to SSL offloading at the load balancer, and then this would have to become 80 (HTTP) |
|
| 281 | + return 80; |
|
| 194 | 282 | } |
| 195 | 283 | |
| 196 | 284 | /** |
| ... | ... | @@ -226,4 +314,5 @@ implements com.sap.sse.landscape.Process<RotatingFileBasedLog, MetricsT> { |
| 226 | 314 | return false; |
| 227 | 315 | } |
| 228 | 316 | } |
| 317 | + |
|
| 229 | 318 | } |
| ... | ... | \ No newline at end of file |
java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/ApacheReverseProxyCluster.java
| ... | ... | @@ -1,13 +1,21 @@ |
| 1 | 1 | package com.sap.sse.landscape.aws.impl; |
| 2 | 2 | |
| 3 | +import java.util.ArrayList; |
|
| 3 | 4 | import java.util.Collections; |
| 4 | 5 | import java.util.HashSet; |
| 6 | +import java.util.Iterator; |
|
| 7 | +import java.util.List; |
|
| 5 | 8 | import java.util.Optional; |
| 6 | 9 | import java.util.Set; |
| 7 | 10 | import java.util.UUID; |
| 8 | - |
|
| 11 | +import java.util.concurrent.TimeoutException; |
|
| 12 | +import java.util.logging.Level; |
|
| 13 | +import java.util.logging.Logger; |
|
| 14 | +import com.sap.sse.landscape.aws.LandscapeConstants; |
|
| 9 | 15 | import com.sap.sse.common.Duration; |
| 10 | 16 | import com.sap.sse.common.Util; |
| 17 | +import com.sap.sse.concurrent.ConsumerWithException; |
|
| 18 | +import com.sap.sse.landscape.Landscape; |
|
| 11 | 19 | import com.sap.sse.landscape.Log; |
| 12 | 20 | import com.sap.sse.landscape.MachineImage; |
| 13 | 21 | import com.sap.sse.landscape.Region; |
| ... | ... | @@ -16,66 +24,144 @@ import com.sap.sse.landscape.SecurityGroup; |
| 16 | 24 | import com.sap.sse.landscape.application.ApplicationProcess; |
| 17 | 25 | import com.sap.sse.landscape.application.ApplicationProcessMetrics; |
| 18 | 26 | import com.sap.sse.landscape.application.Scope; |
| 27 | +import com.sap.sse.landscape.aws.ApplicationLoadBalancer; |
|
| 19 | 28 | import com.sap.sse.landscape.aws.AwsAvailabilityZone; |
| 20 | 29 | import com.sap.sse.landscape.aws.AwsInstance; |
| 21 | 30 | import com.sap.sse.landscape.aws.AwsLandscape; |
| 22 | 31 | import com.sap.sse.landscape.aws.ReverseProxyCluster; |
| 23 | 32 | import com.sap.sse.landscape.aws.Tags; |
| 33 | +import com.sap.sse.landscape.aws.TargetGroup; |
|
| 34 | +import com.sap.sse.landscape.aws.orchestration.StartAwsHost; |
|
| 35 | +import com.sap.sse.shared.util.Wait; |
|
| 24 | 36 | |
| 37 | +import software.amazon.awssdk.services.ec2.model.InstanceStateName; |
|
| 25 | 38 | import software.amazon.awssdk.services.ec2.model.InstanceType; |
| 26 | - |
|
| 27 | -public class ApacheReverseProxyCluster<ShardingKey, MetricsT extends ApplicationProcessMetrics, |
|
| 28 | -ProcessT extends ApplicationProcess<ShardingKey, MetricsT, ProcessT>, LogT extends Log> |
|
| 29 | -extends AbstractApacheReverseProxy<ShardingKey, MetricsT, ProcessT> |
|
| 30 | -implements ReverseProxyCluster<ShardingKey, MetricsT, ProcessT, RotatingFileBasedLog> { |
|
| 39 | +import software.amazon.awssdk.services.elasticloadbalancingv2.model.TagDescription; |
|
| 40 | +import software.amazon.awssdk.services.elasticloadbalancingv2.model.TargetHealth; |
|
| 41 | +import software.amazon.awssdk.services.elasticloadbalancingv2.model.TargetHealthStateEnum; |
|
| 42 | + |
|
| 43 | +public class ApacheReverseProxyCluster<ShardingKey, MetricsT extends ApplicationProcessMetrics, ProcessT extends ApplicationProcess<ShardingKey, MetricsT, ProcessT>, LogT extends Log> |
|
| 44 | + extends AbstractApacheReverseProxy<ShardingKey, MetricsT, ProcessT> |
|
| 45 | + implements ReverseProxyCluster<ShardingKey, MetricsT, ProcessT, RotatingFileBasedLog> { |
|
| 46 | + private static final Logger logger = Logger.getLogger(ApacheReverseProxyCluster.class.getName()); |
|
| 31 | 47 | private Set<AwsInstance<ShardingKey>> hosts; |
| 32 | 48 | |
| 33 | 49 | public ApacheReverseProxyCluster(AwsLandscape<ShardingKey> landscape) { |
| 34 | 50 | super(landscape); |
| 35 | 51 | this.hosts = new HashSet<>(); |
| 36 | 52 | } |
| 37 | - |
|
| 53 | + |
|
| 38 | 54 | @Override |
| 39 | 55 | public Iterable<AwsInstance<ShardingKey>> getHosts() { |
| 40 | 56 | return Collections.unmodifiableCollection(hosts); |
| 41 | 57 | } |
| 42 | - |
|
| 58 | + |
|
| 59 | + /** |
|
| 60 | + * Gets an iterable converting the hosts to ApacheReverseProxies. Assumes the hosts "list/map" is up to date. |
|
| 61 | + */ |
|
| 43 | 62 | private Iterable<ApacheReverseProxy<ShardingKey, MetricsT, ProcessT>> getReverseProxies() { |
| 44 | - return Util.map(hosts, host->new ApacheReverseProxy<>(getLandscape(), host)); |
|
| 63 | + return Util.map(hosts, host -> new ApacheReverseProxy<>(getLandscape(), host)); |
|
| 45 | 64 | } |
| 46 | 65 | |
| 47 | 66 | @Override |
| 48 | 67 | public void addHost(AwsInstance<ShardingKey> host) { |
| 49 | 68 | hosts.add(host); |
| 50 | 69 | } |
| 51 | - |
|
| 70 | + |
|
| 52 | 71 | @Override |
| 53 | - public AwsInstance<ShardingKey> createHost(InstanceType instanceType, AwsAvailabilityZone az, String keyName) { |
|
| 72 | + public AwsInstance<ShardingKey> createHost(String name, InstanceType instanceType, AwsAvailabilityZone az, |
|
| 73 | + String keyName) throws TimeoutException, Exception { |
|
| 54 | 74 | final AwsInstance<ShardingKey> host = getLandscape().launchHost( |
| 55 | - (instanceId, availabilityZone, privateIpAddress, launchTimePoint, landscape) -> new AwsInstanceImpl<ShardingKey>(instanceId, |
|
| 56 | - availabilityZone, privateIpAddress, launchTimePoint, landscape), |
|
| 57 | - getAmiId(), instanceType, az, keyName, |
|
| 58 | - Collections.singleton(getSecurityGroup(az.getRegion())), Optional.of(Tags.with("Name", "ReverseProxy"))); |
|
| 75 | + (instanceId, availabilityZone, privateIpAddress, launchTimePoint, |
|
| 76 | + landscape) -> new AwsInstanceImpl<ShardingKey>(instanceId, availabilityZone, privateIpAddress, |
|
| 77 | + launchTimePoint, landscape), |
|
| 78 | + getAmiId(az.getRegion()), instanceType, az, keyName, getSecurityGroups(az.getRegion()), |
|
| 79 | + Optional.of(Tags.with(StartAwsHost.NAME_TAG_NAME, name).and(LandscapeConstants.DISPOSABLE_PROXY, "") |
|
| 80 | + .and(LandscapeConstants.REVERSE_PROXY_TAG_NAME, "")), |
|
| 81 | + ""); |
|
| 59 | 82 | addHost(host); |
| 83 | + Wait.wait(() -> host.getInstance().state().name().equals(InstanceStateName.RUNNING), |
|
| 84 | + Optional.of(Duration.ofSeconds(60 * 7)), Duration.ofSeconds(30), Level.INFO, |
|
| 85 | + "Is instance in the running state check"); |
|
| 86 | + for (TargetGroup<ShardingKey> targetGroup : getLandscape().getTargetGroups(az.getRegion())) { |
|
| 87 | + targetGroup.getTagDescriptions().forEach(description -> description.tags().forEach(tag -> { |
|
| 88 | + if (tag.key().equals(LandscapeConstants.ALL_REVERSE_PROXIES)) { |
|
| 89 | + final ApplicationLoadBalancer<ShardingKey> loadBalancer = targetGroup.getLoadBalancer(); |
|
| 90 | + if (loadBalancer != null && loadBalancer.getArn().contains(LandscapeConstants.NLB_ARN_CONTAINS)) { |
|
| 91 | + getLandscape().addIpTargetToTargetGroup(targetGroup, Collections.singleton(host)); |
|
| 92 | + logger.info("Added " + host.getPrivateAddress().getHostAddress() + " to NLB target group" |
|
| 93 | + + targetGroup.getTargetGroupArn()); |
|
| 94 | + } else if (loadBalancer != null) { |
|
| 95 | + targetGroup.addTarget(host); |
|
| 96 | + logger.info( |
|
| 97 | + "Added " + host.getInstanceId() + " to target group" + targetGroup.getTargetGroupArn()); |
|
| 98 | + } |
|
| 99 | + } |
|
| 100 | + })); |
|
| 101 | + } |
|
| 60 | 102 | return host; |
| 61 | 103 | } |
| 62 | - |
|
| 104 | + |
|
| 63 | 105 | @Override |
| 64 | - public void removeHost(AwsInstance<ShardingKey> host) { |
|
| 106 | + public void removeHost(AwsInstance<ShardingKey> host, Optional<String> optionalKeyName, |
|
| 107 | + byte[] privateKeyEncryptionPassphrase) throws Exception { |
|
| 65 | 108 | assert Util.contains(getHosts(), host); |
| 66 | 109 | if (Util.size(getHosts()) == 1) { |
| 67 | - throw new IllegalStateException("Trying to remove the last hosts of reverse proxy "+this+". Use terminate() instead"); |
|
| 110 | + throw new IllegalStateException( |
|
| 111 | + "Trying to remove the last hosts of reverse proxy " + this + ". Use terminate() instead"); |
|
| 68 | 112 | } |
| 69 | - getLandscape().terminate(host); // this assumes that the host is running only the reverse proxy process... |
|
| 113 | + AwsInstance<ShardingKey> instanceFromHost = new AwsInstanceImpl<ShardingKey>(host.getInstanceId(), |
|
| 114 | + host.getAvailabilityZone(), host.getPrivateAddress(Landscape.WAIT_FOR_PROCESS_TIMEOUT), |
|
| 115 | + host.getLaunchTimePoint(), getLandscape()); |
|
| 116 | + final List<TargetGroup<ShardingKey>> targetGroupsHostResidesIn = new ArrayList<>(); |
|
| 117 | + for (TargetGroup<ShardingKey> targetGroup : getLandscape() |
|
| 118 | + .getTargetGroups(host.getAvailabilityZone().getRegion())) { |
|
| 119 | + final String loadBalancerArn = targetGroup.getLoadBalancerArn(); |
|
| 120 | + if (loadBalancerArn != null) { |
|
| 121 | + final Iterator<TagDescription> tagDescriptions = targetGroup.getTagDescriptions().iterator(); |
|
| 122 | + while (tagDescriptions.hasNext()) { |
|
| 123 | + final TagDescription tagDescription = tagDescriptions.next(); |
|
| 124 | + if (tagDescription.hasTags()) { |
|
| 125 | + tagDescription.tags().forEach(tag -> { |
|
| 126 | + if (tag.key().equals(LandscapeConstants.ALL_REVERSE_PROXIES) && targetGroup.getRegisteredTargets().containsKey(instanceFromHost)) { |
|
| 127 | + targetGroupsHostResidesIn.add(targetGroup); |
|
| 128 | + if (loadBalancerArn.contains(LandscapeConstants.NLB_ARN_CONTAINS)) { |
|
| 129 | + getLandscape().removeIpTargetFromTargetGroup(targetGroup, Collections.singleton(instanceFromHost)); |
|
| 130 | + } else { |
|
| 131 | + targetGroup.removeTarget(instanceFromHost); |
|
| 132 | + } |
|
| 133 | + } |
|
| 134 | + }); |
|
| 135 | + } |
|
| 136 | + } |
|
| 137 | + } |
|
| 138 | + } |
|
| 139 | + Wait.wait(() -> { |
|
| 140 | + for (TargetGroup<ShardingKey> tg : targetGroupsHostResidesIn) { |
|
| 141 | + final TargetHealth targetHealth = tg.getRegisteredTargets().get(instanceFromHost); |
|
| 142 | + if (targetHealth != null && targetHealth.state().equals(TargetHealthStateEnum.DRAINING)) { |
|
| 143 | + return false; |
|
| 144 | + } |
|
| 145 | + } |
|
| 146 | + return true; |
|
| 147 | + }, Optional.of(Duration.ofSeconds(60 * 10)), Duration.ofSeconds(20), Level.INFO, "Waiting for target to drain"); // Default is 5 minute draining time, as of writing. |
|
| 148 | + ApacheReverseProxy<ShardingKey, MetricsT, ProcessT> proxy = new ApacheReverseProxy<>(getLandscape(), host); |
|
| 149 | + proxy.rotateLogs(optionalKeyName, privateKeyEncryptionPassphrase); |
|
| 150 | + getLandscape().terminate(host); // This assumes that the host is running only the reverse proxy process. |
|
| 70 | 151 | } |
| 71 | 152 | |
| 72 | - private SecurityGroup getSecurityGroup(Region region) { |
|
| 73 | - return getLandscape().getDefaultSecurityGroupForCentralReverseProxy(region); |
|
| 153 | + /** |
|
| 154 | + * Gets the security groups in the region that a reverse proxy instance should or does have. |
|
| 155 | + */ |
|
| 156 | + private Iterable<SecurityGroup> getSecurityGroups(Region region) { |
|
| 157 | + return getLandscape().getDefaultSecurityGroupsForReverseProxy(region); |
|
| 74 | 158 | } |
| 75 | 159 | |
| 76 | - private MachineImage getAmiId() { |
|
| 77 | - // TODO Implement ApacheReverseProxy.getAmiId(...) |
|
| 78 | - return null; |
|
| 160 | + /** |
|
| 161 | + * Gets the latest image in the current region with the correct tag for creating a reverse proxy. |
|
| 162 | + */ |
|
| 163 | + private MachineImage getAmiId(Region region) { |
|
| 164 | + return getLandscape().getLatestImageWithType(region, LandscapeConstants.IMAGE_TYPE_REVERSE_PROXY); |
|
| 79 | 165 | } |
| 80 | 166 | |
| 81 | 167 | @Override |
| ... | ... | @@ -87,85 +173,83 @@ implements ReverseProxyCluster<ShardingKey, MetricsT, ProcessT, RotatingFileBase |
| 87 | 173 | } |
| 88 | 174 | } |
| 89 | 175 | |
| 90 | - @Override |
|
| 91 | - public void setPlainRedirect(String hostname, ProcessT applicationProcess, Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception { |
|
| 92 | - for (final ApacheReverseProxy<ShardingKey, MetricsT, ProcessT> proxy : getReverseProxies()) { |
|
| 93 | - proxy.setPlainRedirect(hostname, applicationProcess, optionalKeyName, privateKeyEncryptionPassphrase); |
|
| 176 | + /** |
|
| 177 | + * Chooses any one instance in the cluster, in the region, to apply the redirect to. Upon a push, a Git hook is |
|
| 178 | + * triggered to propagate the changes to the others in the cluster. |
|
| 179 | + * |
|
| 180 | + * @param redirectSetter |
|
| 181 | + * The ConsumerWithException to apply the necessary redirect to the proxy. |
|
| 182 | + */ |
|
| 183 | + private void setRedirect(ConsumerWithException<ApacheReverseProxy<ShardingKey, MetricsT, ProcessT>> redirectSetter) |
|
| 184 | + throws Exception { |
|
| 185 | + if (getReverseProxies().iterator().hasNext()) { |
|
| 186 | + final ApacheReverseProxy<ShardingKey, MetricsT, ProcessT> proxy = getReverseProxies().iterator().next(); |
|
| 187 | + redirectSetter.accept(proxy); |
|
| 94 | 188 | } |
| 95 | 189 | } |
| 96 | 190 | |
| 97 | 191 | @Override |
| 98 | - public void setHomeRedirect(String hostname, ProcessT applicationProcess, Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception { |
|
| 99 | - for (final ApacheReverseProxy<ShardingKey, MetricsT, ProcessT> proxy : getReverseProxies()) { |
|
| 100 | - proxy.setHomeRedirect(hostname, applicationProcess, optionalKeyName, privateKeyEncryptionPassphrase); |
|
| 101 | - } |
|
| 192 | + public void setPlainRedirect(String hostname, ProcessT applicationProcess, Optional<String> optionalKeyName, |
|
| 193 | + byte[] privateKeyEncryptionPassphrase) throws Exception { |
|
| 194 | + setRedirect(proxy -> proxy.setPlainRedirect(hostname, applicationProcess, optionalKeyName, |
|
| 195 | + privateKeyEncryptionPassphrase)); |
|
| 102 | 196 | } |
| 103 | 197 | |
| 104 | 198 | @Override |
| 105 | - public void setEventRedirect(String hostname, ProcessT applicationProcess, |
|
| 106 | - UUID eventId, Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception { |
|
| 107 | - for (final ApacheReverseProxy<ShardingKey, MetricsT, ProcessT> proxy : getReverseProxies()) { |
|
| 108 | - proxy.setEventRedirect(hostname, applicationProcess, eventId, optionalKeyName, privateKeyEncryptionPassphrase); |
|
| 109 | - } |
|
| 199 | + public void setHomeRedirect(String hostname, ProcessT applicationProcess, Optional<String> optionalKeyName, |
|
| 200 | + byte[] privateKeyEncryptionPassphrase) throws Exception { |
|
| 201 | + setRedirect(proxy -> proxy.setHomeRedirect(hostname, applicationProcess, optionalKeyName, |
|
| 202 | + privateKeyEncryptionPassphrase)); |
|
| 110 | 203 | } |
| 111 | 204 | |
| 112 | 205 | @Override |
| 113 | - public void setEventSeriesRedirect(String hostname, ProcessT applicationProcess, |
|
| 114 | - UUID leaderboardGroupId, Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception { |
|
| 115 | - for (final ApacheReverseProxy<ShardingKey, MetricsT, ProcessT> proxy : getReverseProxies()) { |
|
| 116 | - proxy.setEventSeriesRedirect(hostname, applicationProcess, leaderboardGroupId, optionalKeyName, privateKeyEncryptionPassphrase); |
|
| 117 | - } |
|
| 206 | + public void setEventRedirect(String hostname, ProcessT applicationProcess, UUID eventId, |
|
| 207 | + Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception { |
|
| 208 | + setRedirect(proxy -> proxy.setEventRedirect(hostname, applicationProcess, eventId, optionalKeyName, |
|
| 209 | + privateKeyEncryptionPassphrase)); |
|
| 210 | + } |
|
| 211 | + |
|
| 212 | + @Override |
|
| 213 | + public void setEventSeriesRedirect(String hostname, ProcessT applicationProcess, UUID leaderboardGroupId, |
|
| 214 | + Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception { |
|
| 215 | + setRedirect(proxy -> proxy.setEventSeriesRedirect(hostname, applicationProcess, leaderboardGroupId, |
|
| 216 | + optionalKeyName, privateKeyEncryptionPassphrase)); |
|
| 118 | 217 | } |
| 119 | 218 | |
| 120 | 219 | @Override |
| 121 | 220 | public void setEventArchiveRedirect(String hostname, UUID eventId, Optional<String> optionalKeyName, |
| 122 | 221 | byte[] privateKeyEncryptionPassphrase) throws Exception { |
| 123 | - for (final ApacheReverseProxy<ShardingKey, MetricsT, ProcessT> proxy : getReverseProxies()) { |
|
| 124 | - proxy.setEventArchiveRedirect(hostname, eventId, optionalKeyName, privateKeyEncryptionPassphrase); |
|
| 125 | - } |
|
| 222 | + setRedirect(proxy -> proxy.setEventArchiveRedirect(hostname, eventId, optionalKeyName, |
|
| 223 | + privateKeyEncryptionPassphrase)); |
|
| 126 | 224 | } |
| 127 | 225 | |
| 128 | 226 | @Override |
| 129 | 227 | public void setEventSeriesArchiveRedirect(String hostname, UUID leaderboardGroupId, |
| 130 | 228 | Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception { |
| 131 | - for (final ApacheReverseProxy<ShardingKey, MetricsT, ProcessT> proxy : getReverseProxies()) { |
|
| 132 | - proxy.setEventSeriesArchiveRedirect(hostname, leaderboardGroupId, optionalKeyName, privateKeyEncryptionPassphrase); |
|
| 133 | - } |
|
| 229 | + setRedirect(proxy -> proxy.setEventSeriesArchiveRedirect(hostname, leaderboardGroupId, optionalKeyName, |
|
| 230 | + privateKeyEncryptionPassphrase)); |
|
| 134 | 231 | } |
| 135 | 232 | |
| 136 | 233 | @Override |
| 137 | 234 | public void setHomeArchiveRedirect(String hostname, Optional<String> optionalKeyName, |
| 138 | 235 | byte[] privateKeyEncryptionPassphrase) throws Exception { |
| 139 | - for (final ApacheReverseProxy<ShardingKey, MetricsT, ProcessT> proxy : getReverseProxies()) { |
|
| 140 | - proxy.setHomeArchiveRedirect(hostname, optionalKeyName, privateKeyEncryptionPassphrase); |
|
| 141 | - } |
|
| 236 | + setRedirect(proxy -> proxy.setHomeArchiveRedirect(hostname, optionalKeyName, privateKeyEncryptionPassphrase)); |
|
| 142 | 237 | } |
| 143 | 238 | |
| 144 | 239 | @Override |
| 145 | - public void setScopeRedirect(Scope<ShardingKey> scope, ProcessT applicationProcess) { |
|
| 146 | - for (final ApacheReverseProxy<ShardingKey, MetricsT, ProcessT> proxy : getReverseProxies()) { |
|
| 147 | - proxy.setScopeRedirect(scope, applicationProcess); |
|
| 148 | - } |
|
| 240 | + public void setScopeRedirect(Scope<ShardingKey> scope, ProcessT applicationProcess) throws Exception { |
|
| 241 | + setRedirect(proxy -> proxy.setScopeRedirect(scope, applicationProcess)); |
|
| 149 | 242 | } |
| 150 | 243 | |
| 151 | 244 | @Override |
| 152 | - public void createInternalStatusRedirect(Optional<Duration> optionalTimeout, Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception { |
|
| 153 | - for (final ApacheReverseProxy<ShardingKey, MetricsT, ProcessT> proxy : getReverseProxies()) { |
|
| 154 | - proxy.createInternalStatusRedirect(optionalTimeout, optionalKeyName, privateKeyEncryptionPassphrase); |
|
| 155 | - } |
|
| 245 | + public void removeRedirect(String hostname, Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase) |
|
| 246 | + throws Exception { |
|
| 247 | + setRedirect(proxy -> proxy.removeRedirect(hostname, optionalKeyName, privateKeyEncryptionPassphrase)); |
|
| 156 | 248 | } |
| 157 | 249 | |
| 158 | 250 | @Override |
| 159 | - public void removeRedirect(String hostname, Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception { |
|
| 160 | - for (final ApacheReverseProxy<ShardingKey, MetricsT, ProcessT> proxy : getReverseProxies()) { |
|
| 161 | - proxy.removeRedirect(hostname, optionalKeyName, privateKeyEncryptionPassphrase); |
|
| 162 | - } |
|
| 163 | - } |
|
| 164 | - |
|
| 165 | - @Override |
|
| 166 | - public void removeRedirect(Scope<ShardingKey> scope, Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception { |
|
| 167 | - for (final ApacheReverseProxy<ShardingKey, MetricsT, ProcessT> proxy : getReverseProxies()) { |
|
| 168 | - proxy.removeRedirect(scope, optionalKeyName, privateKeyEncryptionPassphrase); |
|
| 169 | - } |
|
| 251 | + public void removeRedirect(Scope<ShardingKey> scope, Optional<String> optionalKeyName, |
|
| 252 | + byte[] privateKeyEncryptionPassphrase) throws Exception { |
|
| 253 | + setRedirect(proxy -> proxy.removeRedirect(scope, optionalKeyName, privateKeyEncryptionPassphrase)); |
|
| 170 | 254 | } |
| 171 | 255 | } |
java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AwsInstanceImpl.java
| ... | ... | @@ -23,6 +23,7 @@ import com.sap.sse.landscape.aws.AwsAutoScalingGroup; |
| 23 | 23 | import com.sap.sse.landscape.aws.AwsAvailabilityZone; |
| 24 | 24 | import com.sap.sse.landscape.aws.AwsInstance; |
| 25 | 25 | import com.sap.sse.landscape.aws.AwsLandscape; |
| 26 | +import com.sap.sse.landscape.aws.orchestration.StartAwsHost; |
|
| 26 | 27 | import com.sap.sse.landscape.ssh.JCraftLogAdapter; |
| 27 | 28 | import com.sap.sse.landscape.ssh.SSHKeyPair; |
| 28 | 29 | import com.sap.sse.landscape.ssh.SshCommandChannel; |
| ... | ... | @@ -33,6 +34,7 @@ import com.sap.sse.shared.util.Wait; |
| 33 | 34 | import software.amazon.awssdk.services.ec2.model.Instance; |
| 34 | 35 | import software.amazon.awssdk.services.ec2.model.InstanceStateName; |
| 35 | 36 | import software.amazon.awssdk.services.ec2.model.InstanceType; |
| 37 | +import software.amazon.awssdk.services.ec2.model.Tag; |
|
| 36 | 38 | |
| 37 | 39 | public class AwsInstanceImpl<ShardingKey> implements AwsInstance<ShardingKey> { |
| 38 | 40 | private final static Logger logger = Logger.getLogger(AwsInstanceImpl.class.getName()); |
| ... | ... | @@ -43,6 +45,8 @@ public class AwsInstanceImpl<ShardingKey> implements AwsInstance<ShardingKey> { |
| 43 | 45 | private InetAddress publicAddress; |
| 44 | 46 | private final TimePoint launchTimePoint; |
| 45 | 47 | private final AwsLandscape<ShardingKey> landscape; |
| 48 | + private String name; |
|
| 49 | + private String imageId; |
|
| 46 | 50 | |
| 47 | 51 | public AwsInstanceImpl(String instanceId, AwsAvailabilityZone availabilityZone, InetAddress privateAddress, TimePoint launchTimePoint, AwsLandscape<ShardingKey> landscape) { |
| 48 | 52 | this.instanceId = instanceId; |
| ... | ... | @@ -99,6 +103,42 @@ public class AwsInstanceImpl<ShardingKey> implements AwsInstance<ShardingKey> { |
| 99 | 103 | return publicAddress; |
| 100 | 104 | } |
| 101 | 105 | |
| 106 | + /** |
|
| 107 | + * Checks if there is a name tag and returns the value, if it exists. This implementation caches and so may cause |
|
| 108 | + * caching issues, because the name of the instance can change over time. |
|
| 109 | + * |
|
| 110 | + * @return name tag value |
|
| 111 | + */ |
|
| 112 | + public String getNameTag() { |
|
| 113 | + String result = "No name tag found"; |
|
| 114 | + if (name == null) { |
|
| 115 | + final Instance instance = getInstance(); |
|
| 116 | + for (Tag tag : instance.tags()) { |
|
| 117 | + if (tag.key().equals(StartAwsHost.NAME_TAG_NAME)) { |
|
| 118 | + name = tag.value(); |
|
| 119 | + result = name; |
|
| 120 | + break; |
|
| 121 | + } |
|
| 122 | + } |
|
| 123 | + } else { |
|
| 124 | + result = name; |
|
| 125 | + } |
|
| 126 | + return result; |
|
| 127 | + } |
|
| 128 | + |
|
| 129 | + public String getImageId() { |
|
| 130 | + final String result; |
|
| 131 | + if (imageId == null) { |
|
| 132 | + final Instance instance = getInstance(); |
|
| 133 | + imageId = instance.imageId(); |
|
| 134 | + result = imageId; |
|
| 135 | + } else { |
|
| 136 | + result = imageId; |
|
| 137 | + } |
|
| 138 | + return result; |
|
| 139 | + } |
|
| 140 | + |
|
| 141 | + |
|
| 102 | 142 | @Override |
| 103 | 143 | public InetAddress getPrivateAddress() { |
| 104 | 144 | return privateAddress; |
java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AwsLandscapeImpl.java
| ... | ... | @@ -11,6 +11,7 @@ import java.util.Collections; |
| 11 | 11 | import java.util.Comparator; |
| 12 | 12 | import java.util.HashMap; |
| 13 | 13 | import java.util.HashSet; |
| 14 | +import java.util.Iterator; |
|
| 14 | 15 | import java.util.List; |
| 15 | 16 | import java.util.Map; |
| 16 | 17 | import java.util.Map.Entry; |
| ... | ... | @@ -31,10 +32,11 @@ import java.util.logging.Level; |
| 31 | 32 | import java.util.logging.Logger; |
| 32 | 33 | import java.util.regex.Matcher; |
| 33 | 34 | import java.util.regex.Pattern; |
| 34 | - |
|
| 35 | +import java.util.stream.Collectors; |
|
| 35 | 36 | import com.jcraft.jsch.JSch; |
| 36 | 37 | import com.jcraft.jsch.JSchException; |
| 37 | 38 | import com.jcraft.jsch.KeyPair; |
| 39 | +import com.sap.sse.landscape.aws.LandscapeConstants; |
|
| 38 | 40 | import com.sap.sailing.landscape.common.SharedLandscapeConstants; |
| 39 | 41 | import com.sap.sse.common.Duration; |
| 40 | 42 | import com.sap.sse.common.TimePoint; |
| ... | ... | @@ -81,7 +83,6 @@ import com.sap.sse.landscape.ssh.SSHKeyPair; |
| 81 | 83 | import com.sap.sse.mongodb.MongoDBService; |
| 82 | 84 | import com.sap.sse.security.SessionUtils; |
| 83 | 85 | import com.sap.sse.util.ThreadPoolUtil; |
| 84 | - |
|
| 85 | 86 | import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; |
| 86 | 87 | import software.amazon.awssdk.auth.credentials.AwsCredentials; |
| 87 | 88 | import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; |
| ... | ... | @@ -195,11 +196,6 @@ public class AwsLandscapeImpl<ShardingKey> implements AwsLandscape<ShardingKey> |
| 195 | 196 | private static final Logger logger = Logger.getLogger(AwsLandscapeImpl.class.getName()); |
| 196 | 197 | public static final long DEFAULT_DNS_TTL_SECONDS = 60l; |
| 197 | 198 | private static final String DEFAULT_CERTIFICATE_DOMAIN = "*.sapsailing.com"; |
| 198 | - // TODO <config> the "Java Application with Reverse Proxy" security group in eu-west-2 for experimenting; we need this security group per region |
|
| 199 | - private static final String DEFAULT_APPLICATION_SERVER_SECURITY_GROUP_ID_EU_WEST_1 = "sg-eaf31e85"; |
|
| 200 | - private static final String DEFAULT_APPLICATION_SERVER_SECURITY_GROUP_ID_EU_WEST_2 = "sg-0b2afd48960251280"; |
|
| 201 | - private static final String DEFAULT_MONGODB_SECURITY_GROUP_ID_EU_WEST_1 = "sg-0a9bc2fb61f10a342"; |
|
| 202 | - private static final String DEFAULT_MONGODB_SECURITY_GROUP_ID_EU_WEST_2 = "sg-02649c35a73ee0ae5"; |
|
| 203 | 199 | private static final String DEFAULT_NON_DNS_MAPPED_ALB_NAME = "DefDyn"; |
| 204 | 200 | private static final String SAILING_APP_SECURITY_GROUP_NAME = "Sailing Analytics App"; |
| 205 | 201 | private final String accessKeyId; |
| ... | ... | @@ -402,9 +398,11 @@ public class AwsLandscapeImpl<ShardingKey> implements AwsLandscape<ShardingKey> |
| 402 | 398 | final CompletableFuture<String> defaultCertificateArnFuture = getDefaultCertificateArn(alb.getRegion(), DEFAULT_CERTIFICATE_DOMAIN); |
| 403 | 399 | final int httpPort = 80; |
| 404 | 400 | final int httpsPort = 443; |
| 405 | - final ReverseProxyCluster<ShardingKey, MetricsT, ProcessT, RotatingFileBasedLog> reverseProxy = getCentralReverseProxy(alb.getRegion()); |
|
| 406 | - final TargetGroup<ShardingKey> defaultTargetGroup = createTargetGroup(alb.getRegion(), DEFAULT_TARGET_GROUP_PREFIX+alb.getName()+"-"+ProtocolEnum.HTTP.name(), |
|
| 407 | - httpPort, reverseProxy.getHealthCheckPath(), /* healthCheckPort */ httpPort, alb.getArn(), alb.getVpcId()); |
|
| 401 | + final ReverseProxyCluster<ShardingKey, MetricsT, ProcessT, RotatingFileBasedLog> reverseProxy = getReverseProxyCluster(alb.getRegion()); |
|
| 402 | + final HashMap<String, String> tagKeyandValue = new HashMap<>(); |
|
| 403 | + tagKeyandValue.put(LandscapeConstants.ALL_REVERSE_PROXIES, ""); |
|
| 404 | + final TargetGroup<ShardingKey> defaultTargetGroup = createTargetGroup(alb.getRegion(), DEFAULT_TARGET_GROUP_PREFIX + alb.getName() + "-" + ProtocolEnum.HTTP.name(), |
|
| 405 | + httpPort, reverseProxy.getHealthCheckPath(), /* healthCheckPort */ httpPort, alb.getArn(), alb.getVpcId(), tagKeyandValue); |
|
| 408 | 406 | defaultTargetGroup.addTargets(reverseProxy.getHosts()); |
| 409 | 407 | final String defaultCertificateArn = defaultCertificateArnFuture.get(); |
| 410 | 408 | return getLoadBalancingClient(getRegion(alb.getRegion())) |
| ... | ... | @@ -613,8 +611,8 @@ public class AwsLandscapeImpl<ShardingKey> implements AwsLandscape<ShardingKey> |
| 613 | 611 | } |
| 614 | 612 | |
| 615 | 613 | @Override |
| 616 | - public <HostT extends AwsInstance<ShardingKey>> HostT getHostByPrivateIpAddress(com.sap.sse.landscape.Region region, String publicIpAddress, HostSupplier<ShardingKey, HostT> hostSupplier) { |
|
| 617 | - return getHost(region, getInstanceByPrivateIpAddress(region, publicIpAddress), hostSupplier); |
|
| 614 | + public <HostT extends AwsInstance<ShardingKey>> HostT getHostByPrivateIpAddress(com.sap.sse.landscape.Region region, String privateIpAddress, HostSupplier<ShardingKey, HostT> hostSupplier) { |
|
| 615 | + return getHost(region, getInstanceByPrivateIpAddress(region, privateIpAddress), hostSupplier); |
|
| 618 | 616 | } |
| 619 | 617 | |
| 620 | 618 | private Route53Client getRoute53Client() { |
| ... | ... | @@ -1044,27 +1042,36 @@ public class AwsLandscapeImpl<ShardingKey> implements AwsLandscape<ShardingKey> |
| 1044 | 1042 | public TargetGroup<ShardingKey> getTargetGroup(com.sap.sse.landscape.Region region, String targetGroupName) { |
| 1045 | 1043 | return getTargetGroup(region, targetGroupName, /* discover load balancer ARN from target group */ null); |
| 1046 | 1044 | } |
| 1047 | - |
|
| 1045 | + |
|
| 1048 | 1046 | @Override |
| 1049 | 1047 | public TargetGroup<ShardingKey> createTargetGroup(com.sap.sse.landscape.Region region, String targetGroupName, int port, |
| 1050 | 1048 | String healthCheckPath, int healthCheckPort, String loadBalancerArn, String vpcId) { |
| 1049 | + return createTargetGroup(region, targetGroupName, port, healthCheckPath, healthCheckPort, loadBalancerArn, |
|
| 1050 | + vpcId, Collections.emptyMap()); |
|
| 1051 | + } |
|
| 1052 | + |
|
| 1053 | + /** |
|
| 1054 | + * Overloaded method to allow tag-keys and values to be passed. |
|
| 1055 | + */ |
|
| 1056 | + private TargetGroup<ShardingKey> createTargetGroup(com.sap.sse.landscape.Region region, String targetGroupName, int port, |
|
| 1057 | + String healthCheckPath, int healthCheckPort, String loadBalancerArn, String vpcId, Map<String,String> tagKeyAndValues) { |
|
| 1058 | + software.amazon.awssdk.services.elasticloadbalancingv2.model.Tag tags[] = tagKeyAndValues.entrySet().stream() |
|
| 1059 | + .map(entry -> software.amazon.awssdk.services.elasticloadbalancingv2.model.Tag.builder() |
|
| 1060 | + .key(entry.getKey()).value(entry.getValue()).build()) |
|
| 1061 | + .toArray(x -> new software.amazon.awssdk.services.elasticloadbalancingv2.model.Tag[x]); |
|
| 1051 | 1062 | final ElasticLoadBalancingV2Client loadBalancingClient = getLoadBalancingClient(getRegion(region)); |
| 1052 | - final software.amazon.awssdk.services.elasticloadbalancingv2.model.TargetGroup targetGroup = |
|
| 1053 | - loadBalancingClient.createTargetGroup(CreateTargetGroupRequest.builder() |
|
| 1054 | - .name(targetGroupName) |
|
| 1055 | - .healthyThresholdCount(2) |
|
| 1056 | - .unhealthyThresholdCount(2) |
|
| 1057 | - .healthCheckTimeoutSeconds(4) |
|
| 1058 | - .healthCheckEnabled(true) |
|
| 1059 | - .healthCheckIntervalSeconds(5) |
|
| 1060 | - .healthCheckPath(healthCheckPath) |
|
| 1061 | - .healthCheckPort(""+healthCheckPort) |
|
| 1062 | - .healthCheckProtocol(guessProtocolFromPort(healthCheckPort)) |
|
| 1063 | - .port(port) |
|
| 1064 | - .vpcId(vpcId == null ? getVpcId(region) : vpcId) |
|
| 1065 | - .protocol(guessProtocolFromPort(port)) |
|
| 1066 | - .targetType(TargetTypeEnum.INSTANCE) |
|
| 1067 | - .build()).targetGroups().iterator().next(); |
|
| 1063 | + software.amazon.awssdk.services.elasticloadbalancingv2.model.CreateTargetGroupRequest.Builder targetGroupRequestBuilder = CreateTargetGroupRequest |
|
| 1064 | + .builder().name(targetGroupName).healthyThresholdCount(2).unhealthyThresholdCount(2) |
|
| 1065 | + .healthCheckTimeoutSeconds(4).healthCheckEnabled(true).healthCheckIntervalSeconds(10) |
|
| 1066 | + .healthCheckPath(healthCheckPath).healthCheckPort("" + healthCheckPort) |
|
| 1067 | + .healthCheckProtocol(guessProtocolFromPort(healthCheckPort)).port(port) |
|
| 1068 | + .vpcId(vpcId == null ? getVpcId(region) : vpcId).protocol(guessProtocolFromPort(port)) |
|
| 1069 | + .targetType(TargetTypeEnum.INSTANCE); |
|
| 1070 | + if (tags.length > 0) { |
|
| 1071 | + targetGroupRequestBuilder.tags(tags); |
|
| 1072 | + } |
|
| 1073 | + final software.amazon.awssdk.services.elasticloadbalancingv2.model.TargetGroup targetGroup = loadBalancingClient |
|
| 1074 | + .createTargetGroup(targetGroupRequestBuilder.build()).targetGroups().iterator().next(); |
|
| 1068 | 1075 | final String targetGroupArn = targetGroup.targetGroupArn(); |
| 1069 | 1076 | int numberOfRetries = 3; |
| 1070 | 1077 | final Duration TIME_BETWEEN_RETRIES = Duration.ONE_SECOND; |
| ... | ... | @@ -1132,20 +1139,45 @@ public class AwsLandscapeImpl<ShardingKey> implements AwsLandscape<ShardingKey> |
| 1132 | 1139 | final Map<AwsInstance<ShardingKey>, TargetHealth> result = new HashMap<>(); |
| 1133 | 1140 | final Region region = getRegion(targetGroup.getRegion()); |
| 1134 | 1141 | getLoadBalancingClient(region) |
| 1135 | - .describeTargetHealth(DescribeTargetHealthRequest.builder().targetGroupArn(targetGroup.getTargetGroupArn()).build()) |
|
| 1136 | - .targetHealthDescriptions().forEach( |
|
| 1137 | - targetHealthDescription->result.put( |
|
| 1138 | - getHost(targetGroup.getRegion(), targetHealthDescription.target().id(), AwsInstanceImpl::new), targetHealthDescription.targetHealth())); |
|
| 1142 | + .describeTargetHealth( |
|
| 1143 | + DescribeTargetHealthRequest.builder().targetGroupArn(targetGroup.getTargetGroupArn()).build()) |
|
| 1144 | + .targetHealthDescriptions().forEach(targetHealthDescription -> { |
|
| 1145 | + if (targetHealthDescription.target().id().matches("[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+")) { |
|
| 1146 | + AwsInstance<ShardingKey> awsInstance = getHostByPrivateIpAddress(targetGroup.getRegion(), targetHealthDescription.target().id().trim(), |
|
| 1147 | + AwsInstanceImpl::new); |
|
| 1148 | + result.put(awsInstance, targetHealthDescription.targetHealth()); |
|
| 1149 | + } else { |
|
| 1150 | + result.put(getHost(targetGroup.getRegion(), targetHealthDescription.target().id(), |
|
| 1151 | + AwsInstanceImpl::new), targetHealthDescription.targetHealth()); |
|
| 1152 | + } |
|
| 1153 | + }); |
|
| 1139 | 1154 | return result; |
| 1140 | 1155 | } |
| 1141 | 1156 | |
| 1142 | 1157 | @Override |
| 1143 | 1158 | public <MetricsT extends ApplicationProcessMetrics, ProcessT extends AwsApplicationProcess<ShardingKey, MetricsT, ProcessT>> |
| 1159 | + ReverseProxyCluster<ShardingKey, MetricsT, ProcessT, RotatingFileBasedLog> getReverseProxyCluster(com.sap.sse.landscape.Region region) { |
|
| 1160 | + ApacheReverseProxyCluster<ShardingKey, MetricsT, ProcessT, RotatingFileBasedLog> reverseProxyCluster = new ApacheReverseProxyCluster<>(this); |
|
| 1161 | + for (final AwsInstance<ShardingKey> reverseProxyHost : getRunningHostsWithTag(region, LandscapeConstants.REVERSE_PROXY_TAG_NAME, AwsInstanceImpl::new)) { |
|
| 1162 | + reverseProxyCluster.addHost(reverseProxyHost); |
|
| 1163 | + } |
|
| 1164 | + return reverseProxyCluster; |
|
| 1165 | + } |
|
| 1166 | + |
|
| 1167 | + @Override |
|
| 1168 | + public <MetricsT extends ApplicationProcessMetrics, ProcessT extends AwsApplicationProcess<ShardingKey, MetricsT, ProcessT>> |
|
| 1144 | 1169 | ReverseProxyCluster<ShardingKey, MetricsT, ProcessT, RotatingFileBasedLog> getCentralReverseProxy(com.sap.sse.landscape.Region region) { |
| 1145 | 1170 | ApacheReverseProxyCluster<ShardingKey, MetricsT, ProcessT, RotatingFileBasedLog> reverseProxyCluster = new ApacheReverseProxyCluster<>(this); |
| 1146 | - for (final AwsInstance<ShardingKey> reverseProxyHost : getRunningHostsWithTag(region, CENTRAL_REVERSE_PROXY_TAG_NAME, AwsInstanceImpl::new)) { |
|
| 1171 | + for (final AwsInstance<ShardingKey> reverseProxyHost : getRunningHostsWithTag(region, LandscapeConstants.CENTRAL_REVERSE_PROXY_TAG_NAME, AwsInstanceImpl::new)) { |
|
| 1147 | 1172 | reverseProxyCluster.addHost(reverseProxyHost); |
| 1148 | 1173 | } |
| 1174 | + Iterator<AwsInstance<ShardingKey>> iterator = reverseProxyCluster.getHosts().iterator(); |
|
| 1175 | + if (iterator.hasNext()) { |
|
| 1176 | + iterator.next(); |
|
| 1177 | + if (iterator.hasNext()) { |
|
| 1178 | + throw new IllegalStateException("There should only be one central reverse proxy"); |
|
| 1179 | + } |
|
| 1180 | + } |
|
| 1149 | 1181 | return reverseProxyCluster; |
| 1150 | 1182 | } |
| 1151 | 1183 | |
| ... | ... | @@ -1189,6 +1221,18 @@ public class AwsLandscapeImpl<ShardingKey> implements AwsLandscape<ShardingKey> |
| 1189 | 1221 | Iterable<AwsInstance<ShardingKey>> targets) { |
| 1190 | 1222 | getLoadBalancingClient(getRegion(targetGroup.getRegion())).registerTargets(getRegisterTargetsRequestBuilderConsumer(targetGroup, targets)); |
| 1191 | 1223 | } |
| 1224 | + |
|
| 1225 | + @Override |
|
| 1226 | + public void addIpTargetToTargetGroup(TargetGroup<ShardingKey> targetGroup, Iterable<AwsInstance<ShardingKey>> hosts) { |
|
| 1227 | + final TargetDescription[] descriptions = Util.toArray(Util.map(hosts, t->TargetDescription.builder().id(t.getPrivateAddress().getHostAddress()).port(80).build()), new TargetDescription[0]); |
|
| 1228 | + getLoadBalancingClient(getRegion(targetGroup.getRegion())).registerTargets(t->t.targetGroupArn(targetGroup.getTargetGroupArn()).targets(descriptions)); |
|
| 1229 | + } |
|
| 1230 | + |
|
| 1231 | + @Override |
|
| 1232 | + public void removeIpTargetFromTargetGroup(TargetGroup<ShardingKey> targetGroup, Iterable<AwsInstance<ShardingKey>> hosts) { |
|
| 1233 | + final TargetDescription[] descriptions = Util.toArray(Util.map(hosts, t->TargetDescription.builder().id(t.getPrivateAddress().getHostAddress()).port(80).build()), new TargetDescription[0]); |
|
| 1234 | + getLoadBalancingClient(getRegion(targetGroup.getRegion())).deregisterTargets(builder -> builder.targetGroupArn(targetGroup.getTargetGroupArn()).targets(descriptions)); |
|
| 1235 | + } |
|
| 1192 | 1236 | |
| 1193 | 1237 | private TargetDescription[] getTargetDescriptions(Iterable<AwsInstance<ShardingKey>> targets) { |
| 1194 | 1238 | return Util.toArray(Util.map(targets, t->TargetDescription.builder().id(t.getInstanceId()).build()), new TargetDescription[0]); |
| ... | ... | @@ -1226,40 +1270,42 @@ public class AwsLandscapeImpl<ShardingKey> implements AwsLandscape<ShardingKey> |
| 1226 | 1270 | @Override |
| 1227 | 1271 | public SecurityGroup getDefaultSecurityGroupForApplicationHosts(com.sap.sse.landscape.Region region) { |
| 1228 | 1272 | return getSecurityGroupByName(SAILING_APP_SECURITY_GROUP_NAME, region).orElseGet(()->{ |
| 1229 | - final SecurityGroup result; |
|
| 1230 | - if (region.getId().equals(Region.EU_WEST_1.id())) { |
|
| 1231 | - result = getSecurityGroup(DEFAULT_APPLICATION_SERVER_SECURITY_GROUP_ID_EU_WEST_1, region); |
|
| 1232 | - } else if (region.getId().equals(Region.EU_WEST_2.id())) { |
|
| 1233 | - result = getSecurityGroup(DEFAULT_APPLICATION_SERVER_SECURITY_GROUP_ID_EU_WEST_2, region); |
|
| 1234 | - } else { |
|
| 1235 | - result = null; |
|
| 1236 | - } |
|
| 1237 | - return result; |
|
| 1273 | + final List<SecurityGroup> securityGroups = new ArrayList<>(); |
|
| 1274 | + securityGroups.addAll(getSecurityGroupByTag(LandscapeConstants.SAILING_APPLICATION_SG_TAG, region)); |
|
| 1275 | + return securityGroups.isEmpty() ? null : securityGroups.get(0); |
|
| 1238 | 1276 | }); |
| 1239 | 1277 | } |
| 1240 | 1278 | |
| 1241 | 1279 | @Override |
| 1242 | - public SecurityGroup getDefaultSecurityGroupForCentralReverseProxy(com.sap.sse.landscape.Region region) { |
|
| 1243 | - return getDefaultSecurityGroupForApplicationHosts(region); |
|
| 1280 | + public Iterable<SecurityGroup> getDefaultSecurityGroupsForReverseProxy(com.sap.sse.landscape.Region region) { |
|
| 1281 | + return getSecurityGroupByTag(LandscapeConstants.REVERSE_PROXY_SG_TAG, region); |
|
| 1244 | 1282 | } |
| 1245 | 1283 | |
| 1284 | + public List<SecurityGroup> getSecurityGroupByTag(String tag, com.sap.sse.landscape.Region region) { |
|
| 1285 | + final List<software.amazon.awssdk.services.ec2.model.SecurityGroup> securityGroups = getEc2Client( |
|
| 1286 | + getRegion(region)).describeSecurityGroups(sg -> sg.filters(Filter.builder().name("tag-key").values(tag).build())) |
|
| 1287 | + .securityGroups(); |
|
| 1288 | + return securityGroups.stream().map(sg -> new SecurityGroup() { |
|
| 1289 | + @Override |
|
| 1290 | + public String getVpcId() { |
|
| 1291 | + return sg.vpcId(); |
|
| 1292 | + } |
|
| 1293 | + |
|
| 1294 | + @Override |
|
| 1295 | + public String getId() { |
|
| 1296 | + return sg.groupId(); |
|
| 1297 | + } |
|
| 1298 | + }).collect(Collectors.toList()); |
|
| 1299 | + } |
|
| 1300 | + |
|
| 1246 | 1301 | @Override |
| 1247 | 1302 | public SecurityGroup getDefaultSecurityGroupForApplicationLoadBalancer(com.sap.sse.landscape.Region region) { |
| 1248 | 1303 | return getDefaultSecurityGroupForApplicationHosts(region); |
| 1249 | 1304 | } |
| 1250 | 1305 | |
| 1251 | 1306 | @Override |
| 1252 | - public SecurityGroup getDefaultSecurityGroupForMongoDBHosts(com.sap.sse.landscape.Region region) { |
|
| 1253 | - final SecurityGroup result; |
|
| 1254 | - // TODO find a better way, e.g., by tagging, to identify the security group per region to use for MongoDB hosts |
|
| 1255 | - if (region.getId().equals(Region.EU_WEST_1.id())) { |
|
| 1256 | - result = getSecurityGroup(DEFAULT_MONGODB_SECURITY_GROUP_ID_EU_WEST_1, region); |
|
| 1257 | - } else if (region.getId().equals(Region.EU_WEST_2.id())) { |
|
| 1258 | - result = getSecurityGroup(DEFAULT_MONGODB_SECURITY_GROUP_ID_EU_WEST_2, region); |
|
| 1259 | - } else { |
|
| 1260 | - result = null; |
|
| 1261 | - } |
|
| 1262 | - return result; |
|
| 1307 | + public Iterable<SecurityGroup> getDefaultSecurityGroupsForMongoDBHosts(com.sap.sse.landscape.Region region) { |
|
| 1308 | + return getSecurityGroupByTag(LandscapeConstants.MONGO_SG_TAG, region); |
|
| 1263 | 1309 | } |
| 1264 | 1310 | |
| 1265 | 1311 | @Override |
java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/orchestration/RemoveShardingKeyFromShard.java
| ... | ... | @@ -4,7 +4,6 @@ import java.util.HashSet; |
| 4 | 4 | import java.util.Set; |
| 5 | 5 | import java.util.Map.Entry; |
| 6 | 6 | import java.util.logging.Logger; |
| 7 | - |
|
| 8 | 7 | import com.sap.sse.common.Util; |
| 9 | 8 | import com.sap.sse.landscape.application.ApplicationProcess; |
| 10 | 9 | import com.sap.sse.landscape.application.ApplicationProcessMetrics; |
java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/orchestration/StartMongoDBServer.java
| ... | ... | @@ -2,10 +2,8 @@ package com.sap.sse.landscape.aws.orchestration; |
| 2 | 2 | |
| 3 | 3 | import java.io.IOException; |
| 4 | 4 | import java.net.URISyntaxException; |
| 5 | -import java.util.Collections; |
|
| 6 | 5 | import java.util.Optional; |
| 7 | 6 | import java.util.logging.Level; |
| 8 | - |
|
| 9 | 7 | import com.jcraft.jsch.JSchException; |
| 10 | 8 | import com.sap.sse.common.Duration; |
| 11 | 9 | import com.sap.sse.common.Util; |
| ... | ... | @@ -217,7 +215,7 @@ extends StartAwsHost<ShardingKey, AwsInstance<ShardingKey>> { |
| 217 | 215 | public StartMongoDBServer<ShardingKey, ProcessT> build() throws URISyntaxException, JSchException, IOException, InterruptedException { |
| 218 | 216 | setTags(getLandscape().getTagForMongoProcess(getTags().orElse(Tags.empty()), getReplicaSetName(), MongoDBConstants.DEFAULT_PORT)); |
| 219 | 217 | if (!isSecurityGroupsSet()) { |
| 220 | - setSecurityGroups(Collections.singleton(getLandscape().getDefaultSecurityGroupForMongoDBHosts(getRegion()))); |
|
| 218 | + setSecurityGroups(getLandscape().getDefaultSecurityGroupsForMongoDBHosts(getRegion())); |
|
| 221 | 219 | } |
| 222 | 220 | return new StartMongoDBServer<>(this); |
| 223 | 221 | } |
java/com.sap.sse.landscape.test/src/com/sap/sse/landscape/mongodb/test/TestMongoEndpointEquality.java
| ... | ... | @@ -164,5 +164,15 @@ public class TestMongoEndpointEquality { |
| 164 | 164 | public TimePoint getLaunchTimePoint() { |
| 165 | 165 | return null; |
| 166 | 166 | } |
| 167 | + |
|
| 168 | + @Override |
|
| 169 | + public String getNameTag() { |
|
| 170 | + return null; |
|
| 171 | + } |
|
| 172 | + |
|
| 173 | + @Override |
|
| 174 | + public String getImageId() { |
|
| 175 | + return null; |
|
| 176 | + } |
|
| 167 | 177 | } |
| 168 | 178 | } |
java/com.sap.sse.landscape/src/com/sap/sse/landscape/Host.java
| ... | ... | @@ -159,4 +159,16 @@ public interface Host extends WithID { |
| 159 | 159 | default boolean isSharedHost() { |
| 160 | 160 | return false; |
| 161 | 161 | } |
| 162 | + |
|
| 163 | + /** |
|
| 164 | + * Checks if there is a name tag and returns the value, if it exists. |
|
| 165 | + * |
|
| 166 | + * @return name tag value |
|
| 167 | + */ |
|
| 168 | + String getNameTag(); |
|
| 169 | + |
|
| 170 | + /** |
|
| 171 | + * Fetches the AMI id value. We may assume that this is impossible to change, given any amount of time. |
|
| 172 | + */ |
|
| 173 | + String getImageId(); |
|
| 162 | 174 | } |
java/com.sap.sse.landscape/src/com/sap/sse/landscape/Landscape.java
| ... | ... | @@ -35,16 +35,16 @@ public interface Landscape<ShardingKey> { |
| 35 | 35 | SecurityGroup getDefaultSecurityGroupForApplicationLoadBalancer(Region region); |
| 36 | 36 | |
| 37 | 37 | /** |
| 38 | - * @return the security group that shall be assigned by default to any host used as part of the central reverse |
|
| 39 | - * proxy cluster in a region |
|
| 38 | + * @return The security groups that shall be assigned, by default, to any host that is used as part of the central reverse |
|
| 39 | + * proxy cluster in a region. |
|
| 40 | 40 | */ |
| 41 | - SecurityGroup getDefaultSecurityGroupForCentralReverseProxy(Region region); |
|
| 41 | + Iterable<SecurityGroup> getDefaultSecurityGroupsForReverseProxy(Region region); |
|
| 42 | 42 | |
| 43 | 43 | /** |
| 44 | - * @return the security group that shall be assigned by default to any host used as part of a MongoDB replica set; |
|
| 44 | + * @return The security groups that shall be assigned, by default, to any host used as part of a MongoDB replica set; |
|
| 45 | 45 | * we expect public SSH access and local access to the default MongoDB port (usually 27017). |
| 46 | 46 | */ |
| 47 | - SecurityGroup getDefaultSecurityGroupForMongoDBHosts(Region region); |
|
| 47 | + Iterable<SecurityGroup> getDefaultSecurityGroupsForMongoDBHosts(Region region); |
|
| 48 | 48 | |
| 49 | 49 | /** |
| 50 | 50 | * Obtains the default RabbitMQ configuration for the {@code region} specified. If nothing else is specified |
java/com.sap.sse.shared.android/src/com/sap/sse/concurrent/ConsumerWithException.java
| ... | ... | @@ -0,0 +1,6 @@ |
| 1 | +package com.sap.sse.concurrent; |
|
| 2 | + |
|
| 3 | +@FunctionalInterface |
|
| 4 | +public interface ConsumerWithException<T> { |
|
| 5 | + void accept(T t) throws Exception; |
|
| 6 | +} |
wiki/info/landscape/amazon-ec2.md
| ... | ... | @@ -30,9 +30,18 @@ Further ALBs may exist in addition to the default ALB and the NLB for ``sapsaili |
| 30 | 30 | |
| 31 | 31 | ### Apache httpd Webserver and Reverse Proxy |
| 32 | 32 | |
| 33 | -The web server currently exists only as one instance but could now be replicated to other availability zones (AZ)s, entering those other IPs into the ``HTTP-to-sapsailing-dot-com`` target group (and, as will be described further below, to the ``CentralWebServerHTTP*`` (for the "dynamic" ALB in eu-west-1) or ``{ALB-name}-HTTP`` (for all DNS-mapped ALBs) target group of each application load balancer (ALB) in the region). For all of sapsailing.com it does not (no longer) care about SSL and does not need to have an SSL certificate (anymore). In particular, it offers the following services: |
|
| 33 | +The web server currently exists only as one "central" reverse proxy but work is being undertaken to duplicate the essential services, |
|
| 34 | +to improve availability. Only the current central reverse proxy will be non-disposable, hosting the wiki, releases, Git jobs, static and Bugzilla. |
|
| 35 | +Other services, such as p2 remain to be decided. Any traffic to the Hudson build server subdomain gets directed by route 53 to a `DDNSMapped` load balancer (which all route any port 80 traffic to 443), which has a rule pointing to a target group, that contains only the Hudson server. |
|
| 36 | + |
|
| 37 | +The IPs for all reverse proxies will automatically be added to the `CentralWebServerHTTP-Dyn` target group (in the dynamic ALB in eu-west-1) |
|
| 38 | +and to the `DDNSMapped-x-HTTP` (in all the DDNSMapped servers). These are the target groups for the default rules and it ensures availability to the ARCHIVE especially. |
|
| 39 | +Currently, the new approach tags instances with `disposableProxy` to indicate it hosts no vital services. `ReverseProxy` also identifies any reverse proxies. The health check for the target groups would change to trigger a script which returns different error codes: healthy/200 if in the same AZ as the archive (or if the failover archive is in use), whilst unhealthy/503 if in different AZs. This will reduce cross-AZ, archive traffic costs, but maintain availability and load balancing. |
|
| 40 | + |
|
| 41 | +There is hope to also deploy the httpd on already existing instances, which have free resources and a certain tag permitting this |
|
| 42 | +co-deployment. |
|
| 43 | +Most of sapsailing.com no longer cares about SSL and does not need to have an SSL certificate. Sail-insight still does though. The central reverse proxy offers the following services: |
|
| 34 | 44 | |
| 35 | -* hudson.sapsailing.com - a Hudson installation on dev.internal.sapsailing.com |
|
| 36 | 45 | * bugzilla.sapsailing.com - a Bugzilla installation under /usr/lib/bugzilla |
| 37 | 46 | * wiki.sapsailing.com - a Gollum-based Wiki served off our git, see /home/wiki |
| 38 | 47 | * static.sapsailing.com - static content hosted under /home/trac/static |
| ... | ... | @@ -1056,6 +1065,19 @@ Follow these steps to upgrade the AMI: |
| 1056 | 1065 | |
| 1057 | 1066 | ## Terminating AWS Sailing Instances |
| 1058 | 1067 | |
| 1068 | +### Automated approach |
|
| 1069 | + |
|
| 1070 | +A lot of the below has been automated and you can archive from the admin console's landscape panel. It automates much of the procedure, |
|
| 1071 | +including the creation of a httpd `.conf file` in the `conf.d` folder on the reverse proxies, via JSCH/SSH. The file produced is named |
|
| 1072 | +after the domain for the event and it contains |
|
| 1073 | +``` |
|
| 1074 | +Use Event-ARCHIVE 49erEuros2022.sapsailing.com bee070d1-605c-4fff-9d71-7688452abe63 # last part is event uuid. |
|
| 1075 | +``` |
|
| 1076 | +which utilises an in-house macro called Event-ARCHIVE, which creates a proxy pass pointing to the archive. Upon adding to the central |
|
| 1077 | +reverse proxy, changes are pushed to the main branch of a specialised repo (must be main for script to work). Upon push completion, a git `post-receive` hook is triggered (found in `httpdHookScript.sh`) which connects to all reverse proxy instances and runs |
|
| 1078 | +`configuration/sync-repo-and-execute-cmd.sh`. This script fetches changes and merges them, whilst trying to best preserve any changes. |
|
| 1079 | +This is done because live changes can occur to some files such as the 000-macros.conf by the `configuration/switchoverArchive.sh` script, which is installed on each reverse proxy (see the cloud orchestrator page for more details). |
|
| 1080 | + |
|
| 1059 | 1081 | ### ELB Setup with replication server(s) |
| 1060 | 1082 | - Remove all Replica's from the ELB and wait at least 2 minutes until no request reaches their Apache webservers anymore. You can check this with looking at `apachetop` on the respective instances. Let only the Master server live inside the ELB. |
| 1061 | 1083 | - Login to each server instance as `root`-user and stop the java instance with `/home/sailing/servers/server/stop;` |
wiki/projects/cloud-orchestrator.md
| ... | ... | @@ -92,13 +92,12 @@ Loading the time-index fixes for a race from a single MongoDB collection quickly |
| 92 | 92 | |
| 93 | 93 | We should also consider alternatives to MongoDB, at least for the storage of the sensor fixes. [Cassandra](http://cassandra.apache.org/) seems an interesting approach that promises high availability and virtually unlimited scalability. |
| 94 | 94 | |
| 95 | -#### No automatic fail-over for archive server |
|
| 95 | +#### Automatic fail-over for archive server |
|
| 96 | 96 | |
| 97 | -When the archive server fails, a few people get an SMS/text message notification. Manually switching the central reverse proxy configuration in /etc/httpd/conf.d/000-macros.conf is then necessary, followed by a ``service httpd reload`` command to switch to the failover archive server. This process needs automation. A special configuration of "availability" checks between production and failover archive server will be required. We have to figure out where best to put this failover feature: is it something the ALB / target group set-up can do for us? How would the central reverse proxy/proxies route the requests then? |
|
| 97 | +We now automate the failover of the archive server, using `configuration/switchoverArchive.sh`. This script runs on all reverse proxies. It works by switching a PRODUCTION_IP variable to point to either the ARCHIVE_IP or the ARCHIVE_FAILOVER_IP, within the macros file, depending on the status of the primary (checked via multiple curl requests). If changes are made then operators are notified and the config reloaded. Note that this only occurs if the status actually changes, so if it is still unhealthy, then notification/reload do not occur. |
|
| 98 | 98 | |
| 99 | -Alternatively, we could look at other mechanisms for implementing the fail-over functionality. For example, Apache can be configured in "balancer" mode where failover rules can be specified explicitly. |
|
| 100 | - |
|
| 101 | -Alternatively, consider looking at Elastic Beanstalk. |
|
| 99 | +Another note: This approach has some coupling to the archiving process of creating new event-archive macros, because that causes an auto-pull. However, the script prioritises local state, as to maintain archive failover function. |
|
| 100 | +Another +1 note: This script doesn't commit the changes. It only makes them locally. |
|
| 102 | 101 | |
| 103 | 102 | #### No good approach for dynamic scale-up |
| 104 | 103 |