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