AGENTS.md
... ...
@@ -0,0 +1,7 @@
1
+We want all local variables, fields, and parameters to be declared "final" as far as possible.
2
+
3
+We want all literals used as parameters (true, false, numeric literals, Optional.empty() and null) to be prefixed with a comment telling the parameter name.
4
+
5
+We want a single exit point in any method only, so no multiple "return" statements.
6
+
7
+Avoid unnecessary empty lines. If you feel like you need to separate code within a single method into "sections", consider using double-slash comments to explain what the next section is.
configuration/buildAndUpdateProduct.sh
... ...
@@ -671,7 +671,7 @@ if [[ "$@" == "build" ]] || [[ "$@" == "all" ]]; then
671 671
PATH=$PATH:$ANDROID_HOME/platform-tools
672 672
SDK_MANAGER="$ANDROID_HOME/cmdline-tools/8.0/bin/sdkmanager"
673 673
if [ \! -x "$SDK_MANAGER" ]; then
674
- SDK_MANAGER="$ANDROID_HOME/tools/bin/sdkmanager.bat"
674
+ SDK_MANAGER="$ANDROID_HOME/tools/bin/sdkmanager"
675 675
fi
676 676
echo "SDK_MANAGER=${SDK_MANAGER}"
677 677
echo "cmdline-tools:"
configuration/github-download-workflow-artifacts.sh
... ...
@@ -1,8 +1,9 @@
1 1
#!/bin/bash
2
-# Call with two arguments:
2
+# Call with three arguments:
3 3
# - the name of the branch for which you would like
4 4
# to download the latest workflow run's build artifacts
5 5
# - the Github PAT (personal access token)
6
+# - the Github repository {owner}/{repo}, such as SAP/sailing-analytics
6 7
# If found, the build.log.zip and test-result.zip files
7 8
# will be downloaded to the current working directory.
8 9
# The script will exit with status 0 if the workflow's
... ...
@@ -21,21 +22,64 @@ HEADERS_FILE=$( mktemp headersXXXXX )
21 22
NEXT_PAGE="https://api.github.com/repos/${GITHUB_REPOSITORY}/actions/runs?created=${DATE_YESTERDAY/+/%2B}..${UNIX_DATE/+/%2B}&per_page=100"
22 23
ARTIFACTS_JSON=""
23 24
LATEST_RUN_STARTED_AT="0000-00-00T00:00:00Z"
25
+
26
+# Function to check a workflow run and all its previous attempts
27
+# Arguments: $1 = workflow run JSON
28
+# Sets LAST_WORKFLOW_FOR_BRANCH and LATEST_RUN_STARTED_AT if a better match is found
29
+check_run_and_previous_attempts() {
30
+ local RUN_JSON="${1}"
31
+ local CURRENT_RUN="${RUN_JSON}"
32
+
33
+ while [ -n "${CURRENT_RUN}" ] && [ "${CURRENT_RUN}" != "null" ]; do
34
+ local RUN_STATUS=$( echo "${CURRENT_RUN}" | jq -r '.status' )
35
+ local RUN_NAME=$( echo "${CURRENT_RUN}" | jq -r '.name' )
36
+ local RUN_HEAD_BRANCH=$( echo "${CURRENT_RUN}" | jq -r '.head_branch' )
37
+
38
+ # Check if this run matches our criteria
39
+ if [ "${RUN_STATUS}" == "completed" ] && [ "${RUN_NAME}" == "release" ]; then
40
+ if [[ "${RUN_HEAD_BRANCH}" == ${BRANCH}* ]] || [[ "${RUN_HEAD_BRANCH}" == releases/${BRANCH}* ]]; then
41
+ local RUN_STARTED_AT=$( echo "${CURRENT_RUN}" | jq -r '.run_started_at' )
42
+ echo "Found completed run (or previous attempt) started at ${RUN_STARTED_AT}"
43
+ if [[ "${RUN_STARTED_AT}" > "${LATEST_RUN_STARTED_AT}" ]]; then
44
+ echo "This is later than the latest run so far (${LATEST_RUN_STARTED_AT})"
45
+ LAST_WORKFLOW_FOR_BRANCH="${CURRENT_RUN}"
46
+ LATEST_RUN_STARTED_AT="${RUN_STARTED_AT}"
47
+ fi
48
+ fi
49
+ fi
50
+ # Check for previous attempt
51
+ local PREVIOUS_ATTEMPT_URL=$( echo "${CURRENT_RUN}" | jq -r '.previous_attempt_url // empty' )
52
+ if [ -n "${PREVIOUS_ATTEMPT_URL}" ]; then
53
+ echo "Checking previous attempt at ${PREVIOUS_ATTEMPT_URL} ..."
54
+ CURRENT_RUN=$( curl --silent -L -H 'Authorization: Bearer '${BEARER_TOKEN} "${PREVIOUS_ATTEMPT_URL}" 2>/dev/null )
55
+ # Validate we got a proper response
56
+ if [ -z "${CURRENT_RUN}" ] || [ "$( echo "${CURRENT_RUN}" | jq -r '.id // empty' )" == "" ]; then
57
+ echo "Could not fetch previous attempt, stopping chain"
58
+ break
59
+ fi
60
+ else
61
+ break
62
+ fi
63
+ done
64
+}
65
+
24 66
# Now go through the pages as long as we have a non-empty NEXT_PAGE URL and find the completed "release" workflow that was started last
25 67
while [ -n "${NEXT_PAGE}" ]; do
26 68
echo "Trying page ${NEXT_PAGE} ..."
27 69
# Get the artifacts URL of the last workflow run triggered by a branch push for ${BRANCH}:
28
- LAST_WORKFLOW_FOR_BRANCH_ON_PAGE=$( curl -D "${HEADERS_FILE}" --silent -L -H 'Authorization: Bearer '${BEARER_TOKEN} "${NEXT_PAGE}" 2>/dev/null | jq -r '.workflow_runs | map(select(.status == "completed" and .name == "release" and ((.head_branch | startswith("'${BRANCH}'")) or (.head_branch | startswith("releases/'${BRANCH}'"))))) | sort_by(.updated_at) | reverse | .[0]' | sed -e '/^$/d' )
29
- if [ -n "${LAST_WORKFLOW_FOR_BRANCH_ON_PAGE}" -a "${LAST_WORKFLOW_FOR_BRANCH_ON_PAGE}" != "null" ]; then
30
- RUN_STARTED_AT=$( echo "${LAST_WORKFLOW_FOR_BRANCH_ON_PAGE}" | jq -r '.run_started_at' )
31
- echo "Found completed run started at ${RUN_STARTED_AT}"
32
- if [[ "${RUN_STARTED_AT}" > "${LATEST_RUN_STARTED_AT}" ]]; then
33
- echo "This is later than the latest run so far (${LATEST_RUN_STARTED_AT})"
34
- LAST_WORKFLOW_FOR_BRANCH="${LAST_WORKFLOW_FOR_BRANCH_ON_PAGE}"
35
- LATEST_RUN_STARTED_AT="${RUN_STARTED_AT}"
36
- fi
70
+ NEXT_PAGE_CONTENTS=$( curl -D "${HEADERS_FILE}" --silent -L -H 'Authorization: Bearer '${BEARER_TOKEN} "${NEXT_PAGE}" 2>/dev/null )
71
+ # Get all workflow runs that match our branch criteria (completed or not, we'll check status in the function)
72
+ MATCHING_RUNS=$( echo "${NEXT_PAGE_CONTENTS}" | jq -c '.workflow_runs | map(select(.name == "release" and ((.head_branch | startswith("'${BRANCH}'")) or (.head_branch | startswith("releases/'${BRANCH}'"))))) | .[]' 2>/dev/null )
73
+ if [ -n "${MATCHING_RUNS}" ]; then
74
+ # Process each matching run
75
+ while IFS= read -r RUN_JSON; do
76
+ if [ -n "${RUN_JSON}" ] && [ "${RUN_JSON}" != "null" ]; then
77
+ echo "Checking run and its previous attempts..."
78
+ check_run_and_previous_attempts "${RUN_JSON}"
79
+ fi
80
+ done <<< "${MATCHING_RUNS}"
37 81
else
38
- echo "Found no completed run for branch ${BRANCH} on page"
82
+ echo "Found no runs for branch ${BRANCH} on page"
39 83
fi
40 84
NEXT_PAGE=$( grep "^link: .*; rel=\"next\"" "${HEADERS_FILE}" | sed -e 's/^.*<\([^>]*\)>; rel="next".*$/\1/' )
41 85
done
java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/BoatClass.java
... ...
@@ -8,7 +8,7 @@ import com.sap.sse.common.Named;
8 8
import com.sap.sse.common.TimePoint;
9 9
10 10
public interface BoatClass extends Named, IsManagedByCache<SharedDomainFactory<?>> {
11
- final Duration APPROXIMATE_AVERAGE_MANEUVER_DURATION = Duration.ONE_SECOND.times(8); // as discussed with Dennis Gehrlein
11
+ final Duration APPROXIMATE_AVERAGE_MANEUVER_DURATION = Duration.ONE_SECOND.times(10);
12 12
13 13
/**
14 14
* The distance returned by this method should be appropriate for use in
java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/CourseChangeBasedTrackApproximationTest.java
... ...
@@ -69,15 +69,22 @@ public class CourseChangeBasedTrackApproximationTest {
69 69
@Test
70 70
public void testDirectionChangeJustAboveThreshold() {
71 71
final Duration samplingInterval = Duration.ONE_SECOND;
72
- final double aBitOverMinimumManeuverAngleDegrees = boatClass.getManeuverDegreeAngleThreshold() * 1.2;
72
+ final double aBitOverMinimumManeuverAngleDegrees = boatClass.getManeuverDegreeAngleThreshold() * 1.5;
73 73
final TimePoint start = TimePoint.of(10000l);
74 74
final Speed speed = new KnotSpeedImpl(5.0);
75 75
GPSFixMoving next = fix(start.asMillis(), 0, 0, speed.getKnots(), 0);
76 76
track.add(next);
77 77
// perform aBitOverMinimumManeuverAngleDegrees within five fixes:
78 78
final int NUMBER_OF_FIXES_FOR_MANEUVER = 5;
79
+ double cog = 0.0;
79 80
for (int i=0; i<NUMBER_OF_FIXES_FOR_MANEUVER; i++) {
80
- next = travel(next, samplingInterval.asMillis(), speed.getKnots(), ((double) i+1.0)/((double) NUMBER_OF_FIXES_FOR_MANEUVER) * aBitOverMinimumManeuverAngleDegrees);
81
+ cog = ((double) i+1.0)/((double) NUMBER_OF_FIXES_FOR_MANEUVER) * aBitOverMinimumManeuverAngleDegrees;
82
+ next = travel(next, samplingInterval.asMillis(), speed.getKnots(), cog);
83
+ track.add(next);
84
+ }
85
+ // now go straight for the maneuver duration to ensure that the approximation has read buffered fixes up to and including the end of the maneuver:
86
+ for (int i=0; i<boatClass.getApproximateManeuverDurationInMilliseconds()/1000; i++) {
87
+ next = travel(next, Duration.ONE_SECOND.asMillis(), speed.getKnots(), cog);
81 88
track.add(next);
82 89
}
83 90
final Iterable<GPSFixMoving> oneManeuverCandidate = approximation.approximate(start, start.plus(samplingInterval.times(NUMBER_OF_FIXES_FOR_MANEUVER)));
... ...
@@ -117,6 +124,11 @@ public class CourseChangeBasedTrackApproximationTest {
117 124
next = travel(next, samplingInterval.asMillis(), speed.getKnots(), cog);
118 125
track.add(next);
119 126
}
127
+ // now go straight for the maneuver duration to ensure that the approximation has read buffered fixes up to and including the end of the maneuver:
128
+ for (int i=0; i<boatClass.getApproximateManeuverDurationInMilliseconds()/1000; i++) {
129
+ next = travel(next, Duration.ONE_SECOND.asMillis(), speed.getKnots(), cog);
130
+ track.add(next);
131
+ }
120 132
final Iterable<GPSFixMoving> oneManeuverCandidate = approximation.approximate(start, start.plus(samplingInterval.times(NUMBER_OF_FIXES_FOR_NON_MANEUVER+NUMBER_OF_FIXES_FOR_MANEUVER)));
121 133
assertFalse(Util.isEmpty(oneManeuverCandidate));
122 134
assertEquals(1, Util.size(oneManeuverCandidate));
java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/CourseChangeBasedTrackApproximationWithTracTracDataTest.java
... ...
@@ -65,7 +65,9 @@ public class CourseChangeBasedTrackApproximationWithTracTracDataTest extends Onl
65 65
*/
66 66
@Test
67 67
public void testNoDiffBetweenEarlyAndLateInitialization() {
68
- final DynamicGPSFixTrack<Competitor, GPSFixMoving> trackCopy = new DynamicGPSFixMovingTrackImpl<Competitor>(sampleCompetitor, /* millisecondsOverWhichToAverage */ 15000);
68
+ final DynamicGPSFixTrack<Competitor, GPSFixMoving> trackCopy = new DynamicGPSFixMovingTrackImpl<Competitor>(
69
+ sampleCompetitor,
70
+ /* millisecondsOverWhichToAverage */ boatClass.getApproximateManeuverDurationInMilliseconds());
69 71
final CourseChangeBasedTrackApproximation earlyInitApproximation = new CourseChangeBasedTrackApproximation(trackCopy, sampleCompetitor.getBoat().getBoatClass());
70 72
final TimePoint from = sampleTrack.getFirstRawFix().getTimePoint();
71 73
final TimePoint to = sampleTrack.getLastRawFix().getTimePoint();
java/com.sap.sailing.domain/src/com/sap/sailing/domain/maneuverdetection/impl/ManeuverDetectorImpl.java
... ...
@@ -197,7 +197,7 @@ public class ManeuverDetectorImpl extends AbstractManeuverDetectorImpl {
197 197
* Checks whether {@code currentFix} can be grouped together with the previous fixes in order to be regarded as a
198 198
* single maneuver spot. For this, the {@code newCourseChangeDirection must match the direction of provided {@code
199 199
* lastCourseChangeDirection}. Additionally, the distance from {@code previousFix} to {@code currentFix} must be <=
200
- * 3 hull lengths, or the time difference <= getApproximatedManeuverDuration().
200
+ * 4 hull lengths, or the time difference <= getApproximatedManeuverDuration().
201 201
*
202 202
* @param lastCourseChangeDirection The last course within previous three fixes counting from {@code currentFix}
203 203
*
... ...
@@ -212,17 +212,20 @@ public class ManeuverDetectorImpl extends AbstractManeuverDetectorImpl {
212 212
*/
213 213
protected boolean checkDouglasPeuckerFixesGroupable(NauticalSide lastCourseChangeDirection,
214 214
NauticalSide newCourseChangeDirection, GPSFixMoving previousFix, GPSFixMoving currentFix) {
215
+ final boolean result;
215 216
if (lastCourseChangeDirection != newCourseChangeDirection) {
216
- return false;
217
- }
218
- Distance threeHullLengths = trackedRace.getRace().getBoatOfCompetitor(competitor).getBoatClass().getHullLength()
219
- .scale(3);
220
- if (currentFix.getTimePoint().asMillis()
221
- - previousFix.getTimePoint().asMillis() > getApproximateManeuverDuration().asMillis()
222
- && currentFix.getPosition().getDistance(previousFix.getPosition()).compareTo(threeHullLengths) > 0) {
223
- return false;
217
+ result = false;
218
+ } else {
219
+ Distance fourHullLengths = trackedRace.getRace().getBoatOfCompetitor(competitor).getBoatClass().getHullLength().scale(4);
220
+ if (currentFix.getTimePoint().asMillis()
221
+ - previousFix.getTimePoint().asMillis() > getApproximateManeuverDuration().asMillis()
222
+ && currentFix.getPosition().getDistance(previousFix.getPosition()).compareTo(fourHullLengths) > 0) {
223
+ result = false;
224
+ } else {
225
+ result = true;
226
+ }
224 227
}
225
- return true;
228
+ return result;
226 229
}
227 230
228 231
private List<Maneuver> getAllManeuversFromManeuverSpots(List<? extends ManeuverSpot> maneuverSpots) {
java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/CourseChangeBasedTrackApproximation.java
... ...
@@ -6,6 +6,7 @@ import java.io.Serializable;
6 6
import java.util.ArrayList;
7 7
import java.util.Collections;
8 8
import java.util.Comparator;
9
+import java.util.Deque;
9 10
import java.util.LinkedList;
10 11
import java.util.List;
11 12
import java.util.ListIterator;
... ...
@@ -105,6 +106,17 @@ public class CourseChangeBasedTrackApproximation implements Serializable, GPSTra
105 106
private final LinkedList<GPSFixMoving> window;
106 107
107 108
/**
109
+ * New fixes shall be inserted into the {@link FixWindow} only when it is unlikely that newer fixes will still
110
+ * influence the calculation of its {@link GPSFixMoving#getCachedEstimatedSpeed() estimated speed}. This way,
111
+ * the differences between seeing and not seeing newer fixes is reduced, and so it isn't so relevant anymore
112
+ * whether the approximation is updated incrementally as fixes arrive, or after a race has been fully loaded.
113
+ * <p>
114
+ *
115
+ * See also bug 6209.
116
+ */
117
+ private final Deque<GPSFixMoving> queueOfNewFixes;
118
+
119
+ /**
108 120
* We need to remember the speed / bearing as we saw them when we inserted the fixes into the {@link #window}
109 121
* collection. Based on more fixes getting added to the track, things may change. In particular, fixes that may have
110 122
* had a valid speed when inserted may later have their cached speed/bearing invalidated, and computing it again
... ...
@@ -125,6 +137,7 @@ public class CourseChangeBasedTrackApproximation implements Serializable, GPSTra
125 137
126 138
FixWindow() {
127 139
this.window = new LinkedList<>();
140
+ this.queueOfNewFixes = new LinkedList<>();
128 141
this.speedForFixesInWindow = new LinkedList<>();
129 142
this.windowDuration = Duration.NULL;
130 143
// use twice the maneuver duration to also catch slowly-executed gybes
... ...
@@ -160,6 +173,18 @@ public class CourseChangeBasedTrackApproximation implements Serializable, GPSTra
160 173
* or {@code null} if no maneuver candidate became available
161 174
*/
162 175
GPSFixMoving add(GPSFixMoving next) {
176
+ final GPSFixMoving result;
177
+ queueOfNewFixes.add(next); // FIXME bug6209: the queueOfNewFixes needs to remain ordered by fix TimePoint!
178
+ final GPSFixMoving first = queueOfNewFixes.getFirst();
179
+ if (first.getTimePoint().until(next.getTimePoint()).asMillis() > track.getMillisecondsOverWhichToAverageSpeed()/2) {
180
+ result = addOldEnoughFix(queueOfNewFixes.removeFirst());
181
+ } else {
182
+ result = null;
183
+ }
184
+ return result;
185
+ }
186
+
187
+ private GPSFixMoving addOldEnoughFix(GPSFixMoving next) {
163 188
assert window.isEmpty() || !next.getTimePoint().before(window.peekFirst().getTimePoint());
164 189
final GPSFixMoving result;
165 190
final SpeedWithBearing nextSpeed = /* TODO this was the original code that can depend on fixes newer than next: */ next.isEstimatedSpeedCached() ? next.getCachedEstimatedSpeed() : track.getEstimatedSpeed(next.getTimePoint());
java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/StatusServlet.java
... ...
@@ -2,6 +2,9 @@ package com.sap.sailing.gwt.ui.server;
2 2
3 3
import java.io.IOException;
4 4
import java.io.OutputStreamWriter;
5
+import java.lang.management.ManagementFactory;
6
+import java.util.concurrent.ScheduledExecutorService;
7
+import java.util.concurrent.ThreadPoolExecutor;
5 8
6 9
import javax.servlet.ServletContext;
7 10
import javax.servlet.ServletException;
... ...
@@ -20,9 +23,12 @@ import com.mongodb.connection.ClusterDescription;
20 23
import com.mongodb.connection.ServerDescription;
21 24
import com.sap.sailing.server.interfaces.RacingEventService;
22 25
import com.sap.sse.ServerInfo;
26
+import com.sap.sse.common.Duration;
27
+import com.sap.sse.common.Util;
23 28
import com.sap.sse.mongodb.MongoDBService;
24 29
import com.sap.sse.replication.ReplicationService;
25 30
import com.sap.sse.replication.ReplicationStatus;
31
+import com.sap.sse.util.ThreadPoolUtil;
26 32
27 33
public class StatusServlet extends HttpServlet {
28 34
private static final String WAIT_UNTIL_RACES_LOADED = "waitUntilRacesLoaded";
... ...
@@ -58,6 +64,22 @@ public class StatusServlet extends HttpServlet {
58 64
try {
59 65
final JSONObject versionAsJson = ServerInfo.getBuildVersionJson();
60 66
result.putAll(versionAsJson);
67
+ final ScheduledExecutorService defaultBackgroundTaskThreadPoolExecutor = ThreadPoolUtil.INSTANCE.getDefaultBackgroundTaskThreadPoolExecutor();
68
+ if (defaultBackgroundTaskThreadPoolExecutor instanceof ThreadPoolExecutor) {
69
+ final long queueLengthDefaultBackgroundThreadPoolExecutor = ((ThreadPoolExecutor) defaultBackgroundTaskThreadPoolExecutor).getQueue().size();
70
+ result.put("defaultbackgroundthreadpoolexecutorqueuelength", queueLengthDefaultBackgroundThreadPoolExecutor);
71
+ final long nonDelayedQueueLengthDefaultBackgroundThreadPoolExecutor = Util.size(ThreadPoolUtil.INSTANCE.getTasksDelayedByLessThan((ThreadPoolExecutor) defaultBackgroundTaskThreadPoolExecutor, Duration.ONE_SECOND));
72
+ result.put("defaultbackgroundthreadpoolexecutorqueuelengthnondelayed", nonDelayedQueueLengthDefaultBackgroundThreadPoolExecutor);
73
+ }
74
+ final ScheduledExecutorService defaultForegroundTaskThreadPoolExecutor = ThreadPoolUtil.INSTANCE.getDefaultForegroundTaskThreadPoolExecutor();
75
+ if (defaultForegroundTaskThreadPoolExecutor instanceof ThreadPoolExecutor) {
76
+ final long queueLengthDefaultForegroundThreadPoolExecutor = ((ThreadPoolExecutor) defaultForegroundTaskThreadPoolExecutor).getQueue().size();
77
+ result.put("defaultforegroundthreadpoolexecutorqueuelength", queueLengthDefaultForegroundThreadPoolExecutor);
78
+ final long nonDelayedQueueLengthDefaultForegroundThreadPoolExecutor = Util.size(ThreadPoolUtil.INSTANCE.getTasksDelayedByLessThan((ThreadPoolExecutor) defaultForegroundTaskThreadPoolExecutor, Duration.ONE_SECOND));
79
+ result.put("defaultforegroundthreadpoolexecutorqueuelengthnondelayed", nonDelayedQueueLengthDefaultForegroundThreadPoolExecutor);
80
+ }
81
+ final double systemLoadAverageLastMinute = ManagementFactory.getOperatingSystemMXBean().getSystemLoadAverage();
82
+ result.put("systemloadaveragelastminute", systemLoadAverageLastMinute);
61 83
final long numberOfTrackedRacesToRestore = service.getNumberOfTrackedRacesToRestore();
62 84
result.put("numberofracestorestore", numberOfTrackedRacesToRestore);
63 85
final int numberOfTrackedRacesRestored = service.getNumberOfTrackedRacesRestored();
java/com.sap.sailing.landscape.common/src/com/sap/sailing/landscape/common/SharedLandscapeConstants.java
... ...
@@ -125,6 +125,14 @@ public interface SharedLandscapeConstants {
125 125
String SAILING_ANALYTICS_APPLICATION_HOST_TAG = "sailing-analytics-server";
126 126
127 127
String ARCHIVE_SERVER_APPLICATION_REPLICA_SET_NAME = "ARCHIVE";
128
+
129
+ String ARCHIVE_SERVER_INSTANCE_NAME = "SL Archive";
130
+
131
+ String ARCHIVE_SERVER_NEW_CANDIDATE_INSTANCE_NAME = ARCHIVE_SERVER_INSTANCE_NAME+" (New Candidate)";
132
+
133
+ String ARCHIVE_SERVER_FAILOVER_INSTANCE_NAME = ARCHIVE_SERVER_INSTANCE_NAME+" (Failover)";
134
+
135
+ String ARCHIVE_CANDIDATE_SUBDOMAIN = "archive-candidate";
128 136
129 137
/**
130 138
* Value of the {@link #SAILING_ANALYTICS_APPLICATION_HOST_TAG} tag
java/com.sap.sailing.landscape.ui/META-INF/MANIFEST.MF
... ...
@@ -36,6 +36,8 @@ Export-Package: com.google.gwt.user.client.rpc.core.com.sap.sse.landscape.aws.co
36 36
Bundle-ActivationPolicy: lazy
37 37
Import-Package: com.sap.sailing.domain.common,
38 38
com.sap.sailing.server.gateway.interfaces,
39
+ javax.servlet;version="[3.1.0,4.0.0)",
40
+ javax.servlet.http;version="[3.1.0,4.0.0)",
39 41
org.osgi.framework;version="1.8.0",
40 42
org.osgi.util.tracker;version="1.5.1"
41 43
Bundle-Activator: com.sap.sailing.landscape.ui.impl.Activator
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/AbstractApplicationReplicaSetDialog.java
... ...
@@ -1,15 +1,8 @@
1 1
package com.sap.sailing.landscape.ui.client;
2 2
3
-import java.util.Collections;
4
-import java.util.Comparator;
5
-import java.util.LinkedList;
6
-import java.util.List;
7
-
8
-import com.google.gwt.user.client.ui.MultiWordSuggestOracle;
9 3
import com.google.gwt.user.client.ui.SuggestBox;
10 4
import com.google.gwt.user.client.ui.TextBox;
11 5
import com.sap.sailing.landscape.ui.client.i18n.StringMessages;
12
-import com.sap.sse.common.Util;
13 6
import com.sap.sse.gwt.client.ErrorReporter;
14 7
import com.sap.sse.gwt.client.dialog.DataEntryDialog;
15 8
... ...
@@ -45,16 +38,7 @@ public abstract class AbstractApplicationReplicaSetDialog<I extends AbstractAppl
45 38
Iterable<String> releaseNames, StringMessages stringMessages, ErrorReporter errorReporter, Validator<I> validator, DialogCallback<I> callback) {
46 39
super(title, /* message */ null, stringMessages.ok(), stringMessages.cancel(), validator, callback);
47 40
this.stringMessages = stringMessages;
48
- final List<String> releaseNamesAndLatestMaster = new LinkedList<>();
49
- Util.addAll(releaseNames, releaseNamesAndLatestMaster);
50
- final Comparator<String> newestFirstComaprator = (r1, r2)->r2.compareTo(r1);
51
- Collections.sort(releaseNamesAndLatestMaster, newestFirstComaprator);
52
- releaseNamesAndLatestMaster.add(0, stringMessages.latestMasterRelease());
53
- releaseNameBox = createSuggestBox(releaseNamesAndLatestMaster);
54
- if (releaseNameBox.getSuggestOracle() instanceof MultiWordSuggestOracle) {
55
- ((MultiWordSuggestOracle) releaseNameBox.getSuggestOracle()).setComparator(newestFirstComaprator);
56
- }
57
- releaseNameBox.setValue(stringMessages.latestMasterRelease());
41
+ releaseNameBox = LandscapeDialogUtil.createReleaseNameBox(stringMessages, releaseNames, this);
58 42
masterReplicationBearerTokenBox = createTextBox("", 40);
59 43
replicaReplicationBearerTokenBox = createTextBox("", 40);
60 44
}
... ...
@@ -67,11 +51,6 @@ public abstract class AbstractApplicationReplicaSetDialog<I extends AbstractAppl
67 51
return releaseNameBox;
68 52
}
69 53
70
- protected String getReleaseNameBoxValue() {
71
- return (!Util.hasLength(releaseNameBox.getValue()) || Util.equalsWithNull(releaseNameBox.getValue(), stringMessages.latestMasterRelease()))
72
- ? null : releaseNameBox.getValue();
73
- }
74
-
75 54
protected TextBox getMasterReplicationBearerTokenBox() {
76 55
return masterReplicationBearerTokenBox;
77 56
}
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/AbstractNewProcessDialog.java
... ...
@@ -0,0 +1,120 @@
1
+package com.sap.sailing.landscape.ui.client;
2
+
3
+import com.google.gwt.user.client.ui.Grid;
4
+import com.google.gwt.user.client.ui.Label;
5
+import com.google.gwt.user.client.ui.ListBox;
6
+import com.google.gwt.user.client.ui.TextBox;
7
+import com.sap.sailing.landscape.ui.client.i18n.StringMessages;
8
+import com.sap.sse.gwt.client.ErrorReporter;
9
+import com.sap.sse.gwt.client.controls.IntegerBox;
10
+import com.sap.sse.gwt.client.dialog.DataEntryDialog;
11
+
12
+/**
13
+ * Allows the user to specify the parameters required for moving a replica set's master process to a different
14
+ * instance.
15
+ * <p>
16
+ *
17
+ * @author Axel Uhl (d043530)
18
+ *
19
+ */
20
+public abstract class AbstractNewProcessDialog<T> extends DataEntryDialog<T> {
21
+ public static class NewProcessInstructions {
22
+ private final String instanceTypeOrNull;
23
+ private final Integer optionalMemoryInMegabytesOrNull;
24
+ private final Integer optionalMemoryTotalSizeFactorOrNull;
25
+ private final String masterReplicationBearerToken;
26
+ private final String replicaReplicationBearerToken;
27
+
28
+ public NewProcessInstructions(String instanceTypeOrNull,
29
+ String masterReplicationBearerToken, String replicaReplicationBearerToken, Integer optionalMemoryInMegabytesOrNull,
30
+ Integer optionalMemoryTotalSizeFactorOrNull) {
31
+ this.instanceTypeOrNull = instanceTypeOrNull;
32
+ this.optionalMemoryInMegabytesOrNull = optionalMemoryInMegabytesOrNull;
33
+ this.optionalMemoryTotalSizeFactorOrNull = optionalMemoryTotalSizeFactorOrNull;
34
+ this.masterReplicationBearerToken = masterReplicationBearerToken;
35
+ this.replicaReplicationBearerToken = replicaReplicationBearerToken;
36
+ }
37
+ public String getDedicatedInstanceType() {
38
+ return instanceTypeOrNull;
39
+ }
40
+ public Integer getOptionalMemoryInMegabytesOrNull() {
41
+ return optionalMemoryInMegabytesOrNull;
42
+ }
43
+ public Integer getOptionalMemoryTotalSizeFactorOrNull() {
44
+ return optionalMemoryTotalSizeFactorOrNull;
45
+ }
46
+ public String getInstanceTypeOrNull() {
47
+ return instanceTypeOrNull;
48
+ }
49
+ public String getMasterReplicationBearerToken() {
50
+ return masterReplicationBearerToken;
51
+ }
52
+ public String getReplicaReplicationBearerToken() {
53
+ return replicaReplicationBearerToken;
54
+ }
55
+ }
56
+
57
+ protected final StringMessages stringMessages;
58
+ private final ListBox instanceTypeListBox;
59
+ private final Label instanceTypeLabel;
60
+ private final TextBox masterReplicationBearerTokenBox;
61
+ private final TextBox replicaReplicationBearerTokenBox;
62
+ private final IntegerBox memoryInMegabytesBox;
63
+ private final IntegerBox memoryTotalSizeFactorBox;
64
+
65
+ public AbstractNewProcessDialog(String title, String defaultInstanceTypeName,
66
+ LandscapeManagementWriteServiceAsync landscapeManagementService, StringMessages stringMessages,
67
+ ErrorReporter errorReporter, DialogCallback<T> callback) {
68
+ super(title, /* message */ null, stringMessages.ok(), stringMessages.cancel(), /* validator */ null, callback);
69
+ this.stringMessages = stringMessages;
70
+ instanceTypeListBox = LandscapeDialogUtil.createInstanceTypeListBox(this, landscapeManagementService,
71
+ stringMessages, defaultInstanceTypeName, errorReporter, /* canBeDeployedInNlbInstanceBasedTargetGroup */ false);
72
+ instanceTypeLabel = new Label();
73
+ masterReplicationBearerTokenBox = createTextBox("", 40);
74
+ replicaReplicationBearerTokenBox = createTextBox("", 40);
75
+ memoryInMegabytesBox = createIntegerBox(null, 7);
76
+ memoryTotalSizeFactorBox = createIntegerBox(null, 2);
77
+ getMemoryInMegabytesBox().addValueChangeHandler(e->getMemoryTotalSizeFactorBox().setEnabled(e.getValue() == null));
78
+ }
79
+
80
+ @Override
81
+ protected Grid getAdditionalWidget() {
82
+ final Grid result = new Grid(5, 2);
83
+ int row=0;
84
+ result.setWidget(row, 0, getInstanceTypeLabel());
85
+ result.setWidget(row++, 1, getInstanceTypeListBox());
86
+ result.setWidget(row, 0, new Label(stringMessages.bearerTokenForSecurityReplication()));
87
+ result.setWidget(row++, 1, getMasterReplicationBearerTokenBox());
88
+ result.setWidget(row, 0, new Label(stringMessages.replicaReplicationBearerToken()));
89
+ result.setWidget(row++, 1, getReplicaReplicationBearerTokenBox());
90
+ result.setWidget(row, 0, new Label(stringMessages.memoryInMegabytes()));
91
+ result.setWidget(row++, 1, getMemoryInMegabytesBox());
92
+ result.setWidget(row, 0, new Label(stringMessages.memoryTotalSizeFactor()));
93
+ result.setWidget(row++, 1, getMemoryTotalSizeFactorBox());
94
+ return result;
95
+ }
96
+
97
+ protected IntegerBox getMemoryTotalSizeFactorBox() {
98
+ return memoryTotalSizeFactorBox;
99
+ }
100
+
101
+ protected IntegerBox getMemoryInMegabytesBox() {
102
+ return memoryInMegabytesBox;
103
+ }
104
+
105
+ protected TextBox getReplicaReplicationBearerTokenBox() {
106
+ return replicaReplicationBearerTokenBox;
107
+ }
108
+
109
+ protected TextBox getMasterReplicationBearerTokenBox() {
110
+ return masterReplicationBearerTokenBox;
111
+ }
112
+
113
+ protected Label getInstanceTypeLabel() {
114
+ return instanceTypeLabel;
115
+ }
116
+
117
+ protected ListBox getInstanceTypeListBox() {
118
+ return instanceTypeListBox;
119
+ }
120
+}
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/ApplicationReplicaSetsImagesBarCell.java
... ...
@@ -14,6 +14,7 @@ import com.sap.sse.security.ui.client.UserService;
14 14
public class ApplicationReplicaSetsImagesBarCell extends ImagesBarCell {
15 15
static final String ACTION_REMOVE = DefaultActions.DELETE.name();
16 16
static final String ACTION_UPGRADE = "UPGRADE";
17
+ static final String ACTION_ACTIVATE_ARCHIVE_CANDIDATE = "ACTIVATE_ARCHIVE_CANDIDATE";
17 18
static final String ACTION_ARCHIVE = "ARCHIVE";
18 19
static final String ACTION_DEFINE_LANDING_PAGE = "DEFINE_LANDING_PAGE";
19 20
static final String ACTION_CREATE_LOAD_BALANCER_MAPPING = "CREATE_LOAD_BALANGER_MAPPING";
... ...
@@ -60,9 +61,12 @@ public class ApplicationReplicaSetsImagesBarCell extends ImagesBarCell {
60 61
result.add(new ImageSpec(ACTION_LAUNCH_ANOTHER_REPLICA_SET_ON_THIS_MASTER,
61 62
stringMessages.launchAnotherReplicaSetOnThisMaster(),
62 63
IconResources.INSTANCE.launchAnotherReplicaSetOnThisMasterIcon()));
63
- if (!applicationReplicaSet.isLocalReplicaSet(userService) && !applicationReplicaSet.isArchive()) {
64
+ if (!applicationReplicaSet.isLocalReplicaSet(userService)) {
64 65
result.add(new ImageSpec(ACTION_UPGRADE, stringMessages.upgrade(), IconResources.INSTANCE.refreshIcon()));
65 66
}
67
+ if (applicationReplicaSet.isArchive()) {
68
+ result.add(new ImageSpec(ACTION_ACTIVATE_ARCHIVE_CANDIDATE, stringMessages.activateArchiveCandidate(), IconResources.INSTANCE.check()));
69
+ }
66 70
if (!applicationReplicaSet.isArchive()) {
67 71
result.add(
68 72
new ImageSpec(ACTION_ENSURE_ONE_REPLICA_THEN_STOP_REPLICATING_AND_REMOVE_MASTER_FROM_TARGET_GROUPS,
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/CreateApplicationReplicaSetDialog.java
... ...
@@ -319,7 +319,7 @@ public class CreateApplicationReplicaSetDialog extends AbstractApplicationReplic
319 319
return new CreateApplicationReplicaSetInstructions(nameBox.getValue(), sharedMasterInstanceBox.getValue(),
320 320
getDedicatedInstanceTypeListBox().getSelectedValue(),
321 321
getSharedInstanceTypeListBox().getSelectedValue(),
322
- getReleaseNameBoxValue(), dynamicLoadBalancerCheckBox==null?false:dynamicLoadBalancerCheckBox.getValue(),
322
+ LandscapeDialogUtil.getReleaseNameBoxValue(getReleaseNameBox(), stringMessages), dynamicLoadBalancerCheckBox==null?false:dynamicLoadBalancerCheckBox.getValue(),
323 323
getMasterReplicationBearerTokenBox().getValue(), getReplicaReplicationBearerTokenBox().getValue(),
324 324
domainNameBox.getValue(), memoryInMegabytesBox.getValue(), memoryTotalSizeFactorBox.getValue(),
325 325
igtimiRiotPortBox.getValue(), startWithReplicaOnSharedInstanceBox.getValue());
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/LandscapeDialogUtil.java
... ...
@@ -2,10 +2,16 @@ package com.sap.sailing.landscape.ui.client;
2 2
3 3
import java.util.ArrayList;
4 4
import java.util.Collections;
5
+import java.util.Comparator;
6
+import java.util.LinkedList;
7
+import java.util.List;
5 8
6 9
import com.google.gwt.user.client.rpc.AsyncCallback;
7 10
import com.google.gwt.user.client.ui.ListBox;
11
+import com.google.gwt.user.client.ui.MultiWordSuggestOracle;
12
+import com.google.gwt.user.client.ui.SuggestBox;
8 13
import com.sap.sailing.landscape.ui.client.i18n.StringMessages;
14
+import com.sap.sse.common.Util;
9 15
import com.sap.sse.common.util.NaturalComparator;
10 16
import com.sap.sse.gwt.client.ErrorReporter;
11 17
import com.sap.sse.gwt.client.dialog.DataEntryDialog;
... ...
@@ -67,4 +73,23 @@ public class LandscapeDialogUtil {
67 73
}
68 74
}
69 75
}
76
+
77
+ public static SuggestBox createReleaseNameBox(StringMessages stringMessages, Iterable<String> releaseNames, DataEntryDialog<?> dialog) {
78
+ final List<String> releaseNamesAndLatestMaster = new LinkedList<>();
79
+ Util.addAll(releaseNames, releaseNamesAndLatestMaster);
80
+ final Comparator<String> newestFirstComaprator = (r1, r2)->r2.compareTo(r1);
81
+ Collections.sort(releaseNamesAndLatestMaster, newestFirstComaprator);
82
+ releaseNamesAndLatestMaster.add(0, stringMessages.latestMasterRelease());
83
+ SuggestBox releaseNameBox = dialog.createSuggestBox(releaseNamesAndLatestMaster);
84
+ if (releaseNameBox.getSuggestOracle() instanceof MultiWordSuggestOracle) {
85
+ ((MultiWordSuggestOracle) releaseNameBox.getSuggestOracle()).setComparator(newestFirstComaprator);
86
+ }
87
+ releaseNameBox.setValue(stringMessages.latestMasterRelease());
88
+ return releaseNameBox;
89
+ }
90
+
91
+ public static String getReleaseNameBoxValue(SuggestBox releaseNameBox, StringMessages stringMessages) {
92
+ return (!Util.hasLength(releaseNameBox.getValue()) || Util.equalsWithNull(releaseNameBox.getValue(), stringMessages.latestMasterRelease()))
93
+ ? null : releaseNameBox.getValue();
94
+ }
70 95
}
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/LandscapeManagementPanel.java
... ...
@@ -39,6 +39,7 @@ import com.sap.sailing.landscape.ui.client.CreateApplicationReplicaSetDialog.Cre
39 39
import com.sap.sailing.landscape.ui.client.MoveMasterProcessDialog.MoveMasterToOtherInstanceInstructions;
40 40
import com.sap.sailing.landscape.ui.client.SwitchToReplicaOnSharedInstanceDialog.SwitchToReplicaOnSharedInstanceDialogInstructions;
41 41
import com.sap.sailing.landscape.ui.client.UpgradeApplicationReplicaSetDialog.UpgradeApplicationReplicaSetInstructions;
42
+import com.sap.sailing.landscape.ui.client.UpgradeArchiveServerDialog.UpgradeArchiveServerInstructions;
42 43
import com.sap.sailing.landscape.ui.client.i18n.StringMessages;
43 44
import com.sap.sailing.landscape.ui.shared.AmazonMachineImageDTO;
44 45
import com.sap.sailing.landscape.ui.shared.AvailabilityZoneDTO;
... ...
@@ -323,8 +324,19 @@ public class LandscapeManagementPanel extends SimplePanel {
323 324
applicationReplicaSetToArchive -> archiveApplicationReplicaSet(stringMessages,
324 325
regionsTable.getSelectionModel().getSelectedObject(), applicationReplicaSetToArchive));
325 326
applicationReplicaSetsActionColumn.addAction(ApplicationReplicaSetsImagesBarCell.ACTION_UPGRADE,
326
- applicationReplicaSetToUpgrade -> upgradeApplicationReplicaSet(stringMessages,
327
- regionsTable.getSelectionModel().getSelectedObject(), Collections.singleton(applicationReplicaSetToUpgrade)));
327
+ applicationReplicaSetToUpgrade -> {
328
+ if (applicationReplicaSetToUpgrade.isArchive()) {
329
+ upgradeArchiveServer(stringMessages,
330
+ regionsTable.getSelectionModel().getSelectedObject(), applicationReplicaSetToUpgrade);
331
+ } else {
332
+ upgradeApplicationReplicaSet(stringMessages,
333
+ regionsTable.getSelectionModel().getSelectedObject(), Collections.singleton(applicationReplicaSetToUpgrade));
334
+ }
335
+ }
336
+ );
337
+ applicationReplicaSetsActionColumn.addAction(ApplicationReplicaSetsImagesBarCell.ACTION_ACTIVATE_ARCHIVE_CANDIDATE,
338
+ applicationReplicaSetToActivateAsNewArchive -> makeCandidateArchiveServerGoLive(stringMessages,
339
+ regionsTable.getSelectionModel().getSelectedObject(), applicationReplicaSetToActivateAsNewArchive));
328 340
applicationReplicaSetsActionColumn.addAction(ApplicationReplicaSetsImagesBarCell.ACTION_DEFINE_LANDING_PAGE,
329 341
applicationReplicaSetForWhichToDefineLandingPage -> defineLandingPage(stringMessages,
330 342
regionsTable.getSelectionModel().getSelectedObject(), applicationReplicaSetForWhichToDefineLandingPage));
... ...
@@ -1406,7 +1418,7 @@ public class LandscapeManagementPanel extends SimplePanel {
1406 1418
new Timer() {
1407 1419
@Override
1408 1420
public void run() {
1409
- landscapeManagementService.upgradeApplicationReplicaSet(regionId, replicaSet,
1421
+ landscapeManagementService.upgradeApplicationReplicaSet(regionId, replicaSet,
1410 1422
upgradeInstructions.getReleaseNameOrNullForLatestMaster(),
1411 1423
sshKeyManagementPanel.getSelectedKeyPair()==null?null:sshKeyManagementPanel.getSelectedKeyPair().getName(),
1412 1424
sshKeyManagementPanel.getPassphraseForPrivateKeyDecryption() != null
... ...
@@ -1447,6 +1459,86 @@ public class LandscapeManagementPanel extends SimplePanel {
1447 1459
}
1448 1460
});
1449 1461
}
1462
+
1463
+ private void upgradeArchiveServer(StringMessages stringMessages, String regionId,
1464
+ SailingApplicationReplicaSetDTO<String> archiveReplicaSet) {
1465
+ applicationReplicaSetsBusy.setBusy(true);
1466
+ landscapeManagementService.getReleases(new AsyncCallback<ArrayList<ReleaseDTO>>() {
1467
+ @Override
1468
+ public void onFailure(Throwable caught) {
1469
+ applicationReplicaSetsBusy.setBusy(false);
1470
+ errorReporter.reportError(caught.getMessage());
1471
+ }
1472
+
1473
+ @Override
1474
+ public void onSuccess(ArrayList<ReleaseDTO> releases) {
1475
+ new UpgradeArchiveServerDialog(landscapeManagementService, archiveReplicaSet.getMaster().getHost().getInstanceType(),
1476
+ releases.stream().map(r->r.getName())::iterator,
1477
+ stringMessages, errorReporter, new DialogCallback<UpgradeArchiveServerDialog.UpgradeArchiveServerInstructions>() {
1478
+ @Override
1479
+ public void ok(UpgradeArchiveServerInstructions upgradeInstructions) {
1480
+ landscapeManagementService.createArchiveReplicaSet(regionId, archiveReplicaSet, upgradeInstructions.getInstanceTypeOrNull(),
1481
+ upgradeInstructions.getReleaseNameOrNullForLatestMaster(), sshKeyManagementPanel.getSelectedKeyPair()==null?null:sshKeyManagementPanel.getSelectedKeyPair().getName(),
1482
+ sshKeyManagementPanel.getPassphraseForPrivateKeyDecryption() != null
1483
+ ? sshKeyManagementPanel.getPassphraseForPrivateKeyDecryption().getBytes() : null,
1484
+ upgradeInstructions.getMasterReplicationBearerToken(), upgradeInstructions.getReplicaReplicationBearerToken(),
1485
+ upgradeInstructions.getOptionalMemoryInMegabytesOrNull(), upgradeInstructions.getOptionalMemoryTotalSizeFactorOrNull(),
1486
+ new AsyncCallback<Void>() {
1487
+ @Override
1488
+ public void onFailure(Throwable caught) {
1489
+ applicationReplicaSetsBusy.setBusy(false);
1490
+ errorReporter.reportError(caught.getMessage());
1491
+ }
1492
+
1493
+ @Override
1494
+ public void onSuccess(Void result) {
1495
+ applicationReplicaSetsBusy.setBusy(false);
1496
+ Notification.notify(
1497
+ stringMessages.successfullyLaunchedNewArchiveCandidate(
1498
+ archiveReplicaSet.getName(),
1499
+ upgradeInstructions
1500
+ .getReleaseNameOrNullForLatestMaster() == null
1501
+ ? "Default"
1502
+ : upgradeInstructions
1503
+ .getReleaseNameOrNullForLatestMaster()),
1504
+ NotificationType.SUCCESS);
1505
+ }
1506
+ });
1507
+ }
1508
+
1509
+ @Override
1510
+ public void cancel() {
1511
+ applicationReplicaSetsBusy.setBusy(false);
1512
+ }
1513
+ }).show();
1514
+ }
1515
+ });
1516
+ }
1517
+
1518
+ private void makeCandidateArchiveServerGoLive(StringMessages stringMessages, String regionId, SailingApplicationReplicaSetDTO<String> archiveReplicaSetToUpgrade) {
1519
+ if (Window.confirm(stringMessages.reallySwitchToNewArchiveCandidate())) {
1520
+ applicationReplicaSetsBusy.setBusy(true);
1521
+ landscapeManagementService.makeCandidateArchiveServerGoLive(regionId, archiveReplicaSetToUpgrade,
1522
+ sshKeyManagementPanel.getSelectedKeyPair() == null ? null
1523
+ : sshKeyManagementPanel.getSelectedKeyPair().getName(),
1524
+ sshKeyManagementPanel.getPassphraseForPrivateKeyDecryption() != null
1525
+ ? sshKeyManagementPanel.getPassphraseForPrivateKeyDecryption().getBytes()
1526
+ : null,
1527
+ new AsyncCallback<Void>() {
1528
+ @Override
1529
+ public void onFailure(Throwable caught) {
1530
+ applicationReplicaSetsBusy.setBusy(false);
1531
+ errorReporter.reportError(caught.getMessage());
1532
+ }
1533
+
1534
+ @Override
1535
+ public void onSuccess(Void result) {
1536
+ applicationReplicaSetsBusy.setBusy(false);
1537
+ Notification.notify(stringMessages.successfullySwitchedToNewArchiveCandidate(archiveReplicaSetToUpgrade.getName()), NotificationType.SUCCESS);
1538
+ }
1539
+ });
1540
+ }
1541
+ }
1450 1542
1451 1543
private void refreshRegionsTable(UserService userService) {
1452 1544
landscapeManagementService.getRegions(new AsyncCallback<ArrayList<String>>() {
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/LandscapeManagementWriteService.java
... ...
@@ -166,6 +166,11 @@ public interface LandscapeManagementWriteService extends RemoteService {
166 166
SailingApplicationReplicaSetDTO<String> replicaSet, String instanceTypeName,
167 167
String optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception;
168 168
169
+ void createArchiveReplicaSet(
170
+ String regionId, SailingApplicationReplicaSetDTO<String> applicationReplicaSetToUpgrade, String optionalSharedInstanceType, String releaseOrNullForLatestMaster,
171
+ String optionalKeyName, byte[] privateKeyEncryptionPassphrase, String securityReplicationBearerToken, String replicaReplicationBearerToken,
172
+ Integer optionalMemoryInMegabytesOrNull, Integer optionalMemoryTotalSizeFactorOrNull) throws Exception;
173
+
169 174
ArrayList<LeaderboardNameDTO> getLeaderboardNames(SailingApplicationReplicaSetDTO<String> replicaSet, String bearerToken) throws Exception;
170 175
171 176
void addShard(String shardName, ArrayList<LeaderboardNameDTO> selectedLeaderBoardNames, SailingApplicationReplicaSetDTO<String> replicaSet,
... ...
@@ -183,4 +188,8 @@ public interface LandscapeManagementWriteService extends RemoteService {
183 188
String optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception;
184 189
185 190
boolean hasDNSResourceRecordsForReplicaSet(String replicaSetName, String optionalDomainName);
191
+
192
+ void makeCandidateArchiveServerGoLive(String regionId,
193
+ SailingApplicationReplicaSetDTO<String> archiveReplicaSetToUpgrade, String optionalKeyName,
194
+ byte[] privateKeyEncryptionPassphrase) throws Exception;
186 195
}
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/LandscapeManagementWriteServiceAsync.java
... ...
@@ -189,7 +189,15 @@ public interface LandscapeManagementWriteServiceAsync {
189 189
Integer optionalMemoryInMegabytesOrNull, Integer optionalMemoryTotalSizeFactorOrNull, Integer optionalIgtimiRiotPort,
190 190
AwsInstanceDTO optionalPreferredInstanceToDeployUnmanagedReplicaTo,
191 191
AsyncCallback<SailingApplicationReplicaSetDTO<String>> callback);
192
-
192
+
193
+ void createArchiveReplicaSet(String regionId, SailingApplicationReplicaSetDTO<String> applicationReplicaSetToUpgrade,
194
+ String optionalSharedInstanceType, String releaseOrNullForLatestMaster, String optionalKeyName,
195
+ byte[] privateKeyEncryptionPassphrase, String securityReplicationBearerToken, String replicaReplicationBearerToken,
196
+ Integer optionalMemoryInMegabytesOrNull, Integer optionalMemoryTotalSizeFactorOrNull, AsyncCallback<Void> callback);
197
+
198
+ void makeCandidateArchiveServerGoLive(String regionId,
199
+ SailingApplicationReplicaSetDTO<String> archiveReplicaSetToUpgrade, String optionalKeyName,
200
+ byte[] privateKeyEncryptionPassphrase, AsyncCallback<Void> callback);
193 201
/**
194 202
* For the given replica set ensures there is at least one healthy replica, then stops replicating on all replicas and
195 203
* removes the master from the public and master target groups. This can be used as a preparatory action for upgrading
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/MoveMasterProcessDialog.java
... ...
@@ -3,14 +3,9 @@ package com.sap.sailing.landscape.ui.client;
3 3
import com.google.gwt.user.client.ui.CheckBox;
4 4
import com.google.gwt.user.client.ui.Grid;
5 5
import com.google.gwt.user.client.ui.Label;
6
-import com.google.gwt.user.client.ui.ListBox;
7
-import com.google.gwt.user.client.ui.TextBox;
8
-import com.google.gwt.user.client.ui.Widget;
9 6
import com.sap.sailing.landscape.common.SharedLandscapeConstants;
10 7
import com.sap.sailing.landscape.ui.client.i18n.StringMessages;
11 8
import com.sap.sse.gwt.client.ErrorReporter;
12
-import com.sap.sse.gwt.client.controls.IntegerBox;
13
-import com.sap.sse.gwt.client.dialog.DataEntryDialog;
14 9
15 10
/**
16 11
* Allows the user to specify the parameters required for moving a replica set's master process to a different
... ...
@@ -20,115 +15,65 @@ import com.sap.sse.gwt.client.dialog.DataEntryDialog;
20 15
* @author Axel Uhl (d043530)
21 16
*
22 17
*/
23
-public class MoveMasterProcessDialog extends DataEntryDialog<MoveMasterProcessDialog.MoveMasterToOtherInstanceInstructions> {
18
+public class MoveMasterProcessDialog extends AbstractNewProcessDialog<MoveMasterProcessDialog.MoveMasterToOtherInstanceInstructions> {
24 19
25
- public static class MoveMasterToOtherInstanceInstructions {
20
+ public static class MoveMasterToOtherInstanceInstructions extends AbstractNewProcessDialog.NewProcessInstructions {
26 21
private final boolean sharedMasterInstance;
27
- private final String instanceTypeOrNull;
28
- private final Integer optionalMemoryInMegabytesOrNull;
29
- private final Integer optionalMemoryTotalSizeFactorOrNull;
30
- private final String masterReplicationBearerToken;
31
- private final String replicaReplicationBearerToken;
32 22
33 23
public MoveMasterToOtherInstanceInstructions(boolean sharedMasterInstance,
34 24
String instanceTypeOrNull,
35 25
String masterReplicationBearerToken, String replicaReplicationBearerToken, Integer optionalMemoryInMegabytesOrNull,
36 26
Integer optionalMemoryTotalSizeFactorOrNull) {
27
+ super(instanceTypeOrNull, masterReplicationBearerToken, replicaReplicationBearerToken, optionalMemoryInMegabytesOrNull, optionalMemoryTotalSizeFactorOrNull);
37 28
this.sharedMasterInstance = sharedMasterInstance;
38
- this.instanceTypeOrNull = instanceTypeOrNull;
39
- this.optionalMemoryInMegabytesOrNull = optionalMemoryInMegabytesOrNull;
40
- this.optionalMemoryTotalSizeFactorOrNull = optionalMemoryTotalSizeFactorOrNull;
41
- this.masterReplicationBearerToken = masterReplicationBearerToken;
42
- this.replicaReplicationBearerToken = replicaReplicationBearerToken;
43 29
}
44 30
public boolean isSharedMasterInstance() {
45 31
return sharedMasterInstance;
46 32
}
47
- public String getDedicatedInstanceType() {
48
- return instanceTypeOrNull;
49
- }
50
- public Integer getOptionalMemoryInMegabytesOrNull() {
51
- return optionalMemoryInMegabytesOrNull;
52
- }
53
- public Integer getOptionalMemoryTotalSizeFactorOrNull() {
54
- return optionalMemoryTotalSizeFactorOrNull;
55
- }
56
- public String getInstanceTypeOrNull() {
57
- return instanceTypeOrNull;
58
- }
59
- public String getMasterReplicationBearerToken() {
60
- return masterReplicationBearerToken;
61
- }
62
- public String getReplicaReplicationBearerToken() {
63
- return replicaReplicationBearerToken;
64
- }
65 33
}
66 34
67
- private final StringMessages stringMessages;
68 35
private final CheckBox sharedMasterInstanceBox;
69
- private final ListBox instanceTypeListBox;
70
- private final Label instanceTypeLabel;
71
- private final TextBox masterReplicationBearerTokenBox;
72
- private final TextBox replicaReplicationBearerTokenBox;
73
- private final IntegerBox memoryInMegabytesBox;
74
- private final IntegerBox memoryTotalSizeFactorBox;
75 36
private boolean memoryAsFactorToTotalMemoryAdjusted;
76 37
77 38
public MoveMasterProcessDialog(LandscapeManagementWriteServiceAsync landscapeManagementService,
78
- StringMessages stringMessages, ErrorReporter errorReporter, DialogCallback<MoveMasterToOtherInstanceInstructions> callback) {
79
- super(stringMessages.moveMasterToOtherInstance(), /* message */ null, stringMessages.ok(), stringMessages.cancel(), /* validator */ null, callback);
80
- this.stringMessages = stringMessages;
81
- instanceTypeListBox = LandscapeDialogUtil.createInstanceTypeListBox(this, landscapeManagementService,
82
- stringMessages, SharedLandscapeConstants.DEFAULT_DEDICATED_INSTANCE_TYPE_NAME, errorReporter, /* canBeDeployedInNlbInstanceBasedTargetGroup */ false);
83
- instanceTypeLabel = new Label();
84
- masterReplicationBearerTokenBox = createTextBox("", 40);
85
- replicaReplicationBearerTokenBox = createTextBox("", 40);
86
- memoryInMegabytesBox = createIntegerBox(null, 7);
87
- memoryTotalSizeFactorBox = createIntegerBox(null, 2);
88
- memoryTotalSizeFactorBox.addValueChangeHandler(e->memoryAsFactorToTotalMemoryAdjusted=true);
89
- memoryInMegabytesBox.addValueChangeHandler(e->memoryTotalSizeFactorBox.setEnabled(e.getValue() == null));
39
+ StringMessages stringMessages, ErrorReporter errorReporter,
40
+ DialogCallback<MoveMasterToOtherInstanceInstructions> callback) {
41
+ super(stringMessages.moveMasterToOtherInstance(), SharedLandscapeConstants.DEFAULT_DEDICATED_INSTANCE_TYPE_NAME,
42
+ landscapeManagementService, stringMessages, errorReporter, callback);
43
+ getMemoryTotalSizeFactorBox().addValueChangeHandler(e->memoryAsFactorToTotalMemoryAdjusted=true);
90 44
sharedMasterInstanceBox = createCheckbox(stringMessages.sharedMasterInstance());
91 45
sharedMasterInstanceBox.addValueChangeHandler(e->updateInstanceTypeBasedOnSharedMasterInstanceBox());
92 46
updateInstanceTypeBasedOnSharedMasterInstanceBox();
93 47
}
94 48
95 49
private void updateInstanceTypeBasedOnSharedMasterInstanceBox() {
96
- instanceTypeLabel.setText(sharedMasterInstanceBox.getValue() ? stringMessages.sharedMasterInstanceType() : stringMessages.dedicatedInstanceType());
97
- LandscapeDialogUtil.selectInstanceType(instanceTypeListBox,
50
+ getInstanceTypeLabel().setText(sharedMasterInstanceBox.getValue() ? stringMessages.sharedMasterInstanceType() : stringMessages.dedicatedInstanceType());
51
+ LandscapeDialogUtil.selectInstanceType(getInstanceTypeListBox(),
98 52
sharedMasterInstanceBox.getValue() ? SharedLandscapeConstants.DEFAULT_SHARED_INSTANCE_TYPE_NAME : SharedLandscapeConstants.DEFAULT_DEDICATED_INSTANCE_TYPE_NAME);
99 53
if (!memoryAsFactorToTotalMemoryAdjusted) {
100 54
if (sharedMasterInstanceBox.getValue()) {
101
- memoryTotalSizeFactorBox.setValue(SharedLandscapeConstants.DEFAULT_NUMBER_OF_PROCESSES_IN_MEMORY);
55
+ getMemoryTotalSizeFactorBox().setValue(SharedLandscapeConstants.DEFAULT_NUMBER_OF_PROCESSES_IN_MEMORY);
102 56
} else {
103
- memoryTotalSizeFactorBox.setText("");
57
+ getMemoryTotalSizeFactorBox().setText("");
104 58
}
105 59
}
106 60
}
107 61
108 62
@Override
109
- protected Widget getAdditionalWidget() {
110
- final Grid result = new Grid(6, 2);
63
+ protected Grid getAdditionalWidget() {
64
+ final Grid result = super.getAdditionalWidget();
111 65
int row=0;
66
+ result.insertRow(row);
112 67
result.setWidget(row, 0, new Label(stringMessages.sharedMasterInstance()));
113 68
result.setWidget(row++, 1, sharedMasterInstanceBox);
114
- result.setWidget(row, 0, instanceTypeLabel);
115
- result.setWidget(row++, 1, instanceTypeListBox);
116
- result.setWidget(row, 0, new Label(stringMessages.bearerTokenForSecurityReplication()));
117
- result.setWidget(row++, 1, masterReplicationBearerTokenBox);
118
- result.setWidget(row, 0, new Label(stringMessages.replicaReplicationBearerToken()));
119
- result.setWidget(row++, 1, replicaReplicationBearerTokenBox);
120
- result.setWidget(row, 0, new Label(stringMessages.memoryInMegabytes()));
121
- result.setWidget(row++, 1, memoryInMegabytesBox);
122
- result.setWidget(row, 0, new Label(stringMessages.memoryTotalSizeFactor()));
123
- result.setWidget(row++, 1, memoryTotalSizeFactorBox);
124 69
return result;
125 70
}
126 71
127 72
@Override
128 73
protected MoveMasterToOtherInstanceInstructions getResult() {
129 74
return new MoveMasterToOtherInstanceInstructions(sharedMasterInstanceBox.getValue(),
130
- instanceTypeListBox.getSelectedValue(),
131
- masterReplicationBearerTokenBox.getValue(), replicaReplicationBearerTokenBox.getValue(),
132
- memoryInMegabytesBox.getValue(), memoryTotalSizeFactorBox.getValue());
75
+ getInstanceTypeListBox().getSelectedValue(),
76
+ getMasterReplicationBearerTokenBox().getValue(), getReplicaReplicationBearerTokenBox().getValue(),
77
+ getMemoryInMegabytesBox().getValue(), getMemoryTotalSizeFactorBox().getValue());
133 78
}
134 79
}
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/UpgradeApplicationReplicaSetDialog.java
... ...
@@ -40,6 +40,8 @@ public class UpgradeApplicationReplicaSetDialog extends AbstractApplicationRepli
40 40
41 41
@Override
42 42
protected UpgradeApplicationReplicaSetInstructions getResult() {
43
- return new UpgradeApplicationReplicaSetInstructions(getReleaseNameBoxValue(), getMasterReplicationBearerTokenBox().getValue(), getReplicaReplicationBearerTokenBox().getValue());
43
+ return new UpgradeApplicationReplicaSetInstructions(
44
+ LandscapeDialogUtil.getReleaseNameBoxValue(getReleaseNameBox(), stringMessages),
45
+ getMasterReplicationBearerTokenBox().getValue(), getReplicaReplicationBearerTokenBox().getValue());
44 46
}
45 47
}
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/UpgradeArchiveServerDialog.java
... ...
@@ -0,0 +1,57 @@
1
+package com.sap.sailing.landscape.ui.client;
2
+
3
+import com.google.gwt.user.client.ui.FocusWidget;
4
+import com.google.gwt.user.client.ui.Grid;
5
+import com.google.gwt.user.client.ui.Label;
6
+import com.google.gwt.user.client.ui.SuggestBox;
7
+import com.sap.sailing.landscape.ui.client.i18n.StringMessages;
8
+import com.sap.sse.gwt.client.ErrorReporter;
9
+
10
+public class UpgradeArchiveServerDialog extends AbstractNewProcessDialog<UpgradeArchiveServerDialog.UpgradeArchiveServerInstructions> {
11
+
12
+ public static class UpgradeArchiveServerInstructions extends AbstractNewProcessDialog.NewProcessInstructions {
13
+ private final String releaseNameOrNullForLatestMaster;
14
+
15
+ public UpgradeArchiveServerInstructions(String releaseNameOrNullForLatestMaster,
16
+ String masterReplicationBearerToken, String replicaReplicationBearerToken, String optionalInstanceType,
17
+ Integer optionalMemoryInMegabytesOrNull, Integer optionalMemoryTotalSizeFactorOrNull) {
18
+ super(optionalInstanceType, masterReplicationBearerToken, replicaReplicationBearerToken, optionalMemoryInMegabytesOrNull, optionalMemoryTotalSizeFactorOrNull);
19
+ this.releaseNameOrNullForLatestMaster = releaseNameOrNullForLatestMaster;
20
+ }
21
+
22
+ public String getReleaseNameOrNullForLatestMaster() {
23
+ return releaseNameOrNullForLatestMaster;
24
+ }
25
+ }
26
+
27
+ private final SuggestBox releaseNameBox;
28
+
29
+ public UpgradeArchiveServerDialog(LandscapeManagementWriteServiceAsync landscapeManagementService, String defaultInstanceTypeName,
30
+ Iterable<String> releaseNames,
31
+ StringMessages stringMessages, ErrorReporter errorReporter, DialogCallback<UpgradeArchiveServerInstructions> callback) {
32
+ super(stringMessages.upgradeArchiveServer(), defaultInstanceTypeName, landscapeManagementService, stringMessages, errorReporter, callback);
33
+ releaseNameBox = LandscapeDialogUtil.createReleaseNameBox(stringMessages, releaseNames, this);
34
+ }
35
+
36
+ @Override
37
+ protected Grid getAdditionalWidget() {
38
+ final Grid result = super.getAdditionalWidget();
39
+ int row=0;
40
+ result.insertRow(row);
41
+ result.setWidget(row, 0, new Label(stringMessages.release()));
42
+ result.setWidget(row++, 1, releaseNameBox);
43
+ return result;
44
+ }
45
+
46
+ @Override
47
+ public FocusWidget getInitialFocusWidget() {
48
+ return releaseNameBox.getValueBox();
49
+ }
50
+
51
+ @Override
52
+ protected UpgradeArchiveServerInstructions getResult() {
53
+ return new UpgradeArchiveServerInstructions(LandscapeDialogUtil.getReleaseNameBoxValue(releaseNameBox, stringMessages), getMasterReplicationBearerTokenBox().getValue(),
54
+ getReplicaReplicationBearerTokenBox().getValue(), getInstanceTypeListBox().getSelectedValue(),
55
+ getMemoryInMegabytesBox().getValue(), getMemoryTotalSizeFactorBox().getValue());
56
+ }
57
+}
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/i18n/StringMessages.java
... ...
@@ -102,6 +102,7 @@ com.sap.sse.gwt.adminconsole.StringMessages {
102 102
String successfullyUpgradedApplicationReplicaSet(String name, String version);
103 103
String upgradingApplicationReplicaSetFailed(String name);
104 104
String upgradeApplicationReplicaSet();
105
+ String upgradeArchiveServer();
105 106
String successfullyArchivedReplicaSet(String name);
106 107
String removeArchivedReplicaSet();
107 108
String bearerTokenOrNullForApplicationReplicaSetToArchive(String replicaSetName);
... ...
@@ -179,4 +180,8 @@ com.sap.sse.gwt.adminconsole.StringMessages {
179 180
String privateIp();
180 181
String igtimiRiotPort();
181 182
String examplePort(int examplePort);
183
+ String successfullyLaunchedNewArchiveCandidate(String replicaSetName, String releaseName);
184
+ String successfullySwitchedToNewArchiveCandidate(String replicaSetName);
185
+ String activateArchiveCandidate();
186
+ String reallySwitchToNewArchiveCandidate();
182 187
}
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/i18n/StringMessages.properties
... ...
@@ -91,6 +91,7 @@ latestMasterRelease=Latest master release
91 91
successfullyUpgradedApplicationReplicaSet=Successfully upgraded application replica set {0} to version {1}.
92 92
upgradingApplicationReplicaSetFailed=Upgrading application replica set {0} failed.
93 93
upgradeApplicationReplicaSet=Upgrade application replica set
94
+upgradeArchiveServer=Upgrade archive server
94 95
successfullyArchivedReplicaSet=Successfully archived replica set {0}.
95 96
removeArchivedReplicaSet=Remove archived replica set after successful verification?
96 97
bearerTokenOrNullForApplicationReplicaSetToArchive=Bearer token for application replica set {0} (leave empty for current user)
... ...
@@ -167,4 +168,8 @@ runOnExisting=Run on an existing, running instance
167 168
publicIp=Public IP address
168 169
privateIp=Private IP address
169 170
igtimiRiotPort=Igtimi Riot Port
170
-examplePort=e.g., {0}
... ...
\ No newline at end of file
0
+examplePort=e.g., {0}
1
+successfullyLaunchedNewArchiveCandidate=Successfully launched new {0} candidate with release {1}. You will receive an e-mail when the candidate is ready for spot checks and rotation to production. This can take several hours, depending on the number of events to load.
2
+successfullySwitchedToNewArchiveCandidate=Successfully switched to new {0} server
3
+activateArchiveCandidate=Activate ARCHIVE candidate (must have run an upgrade before and received the success e-mail!)
4
+reallySwitchToNewArchiveCandidate=Really switch to new ARCHIVE candidate? You confirm that you have previously received a success notification from a prior ARCHIVE upgrade request and that you have done a few spot checks to ensure the ARCHIVE candidate looks good.
... ...
\ No newline at end of file
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/i18n/StringMessages_de.properties
... ...
@@ -91,6 +91,7 @@ latestMasterRelease=Aktuellste Standard-Version
91 91
successfullyUpgradedApplicationReplicaSet=Anwendungs-Cluster {0} wurde erfolgreich auf Version {1} aktualisiert.
92 92
upgradingApplicationReplicaSetFailed=Aktualisierung des Anwendungs-Clusters {0} ist fehlgeschlagen.
93 93
upgradeApplicationReplicaSet=Anwendungs-Cluster aktualisieren
94
+ugradeArchvieServer=Archiv-Server aktualisieren
94 95
successfullyArchivedReplicaSet=Anwendungs-Cluster {0} erfolgreich archiviert.
95 96
removeArchivedReplicaSet=Anwendungs-Cluster nach dem Archivieren entfernen?
96 97
bearerTokenOrNullForApplicationReplicaSetToArchive=Token für zu archivierendes Anwendungs-Cluster {0} (leer für aktuellen Benutzer)
... ...
@@ -166,4 +167,8 @@ runOnExisting=Auf anderem laufenden Server ausführen
166 167
publicIp=Öffentlich IP-Adresse
167 168
privateIP=Private IP-Adresse
168 169
igtimiRiotPort=Igtimi Riot Port
169
-examplePort=z.B. {0}
... ...
\ No newline at end of file
0
+examplePort=z.B. {0}
1
+successfullyLaunchedNewArchiveCandidate=Neuen Kandidaten für {0} mit Version {1} gestartet. Es erfolgt eine Benachrichtigung per e-Mail. Das kann, je nach Umfang der zu ladenden Daten, etliche Stunden dauern.
2
+successfullySwitchedToNewArchiveCandidate=Erfolgreich auf neuen Server {0} umgeschaltet
3
+activateArchiveCandidate=ARCHIVE-Kandidaten aktivieren (zuvor muss ein Upgrade-Versuch per e-Mail als erfolgreich markiert worden sein!)
4
+reallySwitchToNewArchiveCandidate=Wirklich auf dne neuen ARCHIVE-Kandidaten umschalten? Du bestätigst damit, dass Du eine Erfolgs-Benachrichtigung zu einer vorangegangenen Update-Prozedur für den ARCHIVE-Server erhalten hast und Du Dich wenigstens stichprobenartig von der Korrektheit des neuen Kandidaten überzeugt hast.
... ...
\ No newline at end of file
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/server/LandscapeManagementWriteServiceImpl.java
... ...
@@ -4,6 +4,7 @@ import java.io.IOException;
4 4
import java.net.InetAddress;
5 5
import java.net.MalformedURLException;
6 6
import java.net.URISyntaxException;
7
+import java.net.URL;
7 8
import java.net.UnknownHostException;
8 9
import java.util.ArrayList;
9 10
import java.util.Arrays;
... ...
@@ -73,6 +74,7 @@ import com.sap.sse.common.Util;
73 74
import com.sap.sse.common.Util.Pair;
74 75
import com.sap.sse.common.Util.Triple;
75 76
import com.sap.sse.gwt.server.ResultCachingProxiedRemoteServiceServlet;
77
+import com.sap.sse.landscape.DefaultProcessConfigurationVariables;
76 78
import com.sap.sse.landscape.Host;
77 79
import com.sap.sse.landscape.Landscape;
78 80
import com.sap.sse.landscape.Release;
... ...
@@ -99,6 +101,7 @@ import com.sap.sse.landscape.aws.orchestration.CreateDynamicLoadBalancerMapping;
99 101
import com.sap.sse.landscape.aws.orchestration.CreateLoadBalancerMapping;
100 102
import com.sap.sse.landscape.aws.orchestration.StartMongoDBServer;
101 103
import com.sap.sse.landscape.common.shared.SecuredLandscapeTypes;
104
+import com.sap.sse.landscape.mongodb.Database;
102 105
import com.sap.sse.landscape.mongodb.MongoEndpoint;
103 106
import com.sap.sse.landscape.mongodb.MongoProcess;
104 107
import com.sap.sse.landscape.mongodb.MongoProcessInReplicaSet;
... ...
@@ -295,10 +298,10 @@ public class LandscapeManagementWriteServiceImpl extends ResultCachingProxiedRem
295 298
private ReverseProxyDTO convertToReverseProxyDTO(String region, Map<AwsInstance<String>, TargetHealth> healths,
296 299
AwsInstance<String> instance, boolean isDisposable) {
297 300
return new ReverseProxyDTO(instance.getInstanceId(),
298
- instance.getPrivateAddress().getHostAddress(), instance.getPublicAddress().getHostAddress(),
299
- region, instance.getLaunchTimePoint(), instance.isSharedHost(),
300
- instance.getNameTag(), instance.getImageId(), extractHealth(healths, instance),
301
- isDisposable, new AvailabilityZoneDTO(instance.getAvailabilityZone().getName(), instance.getRegion().getId(), instance.getAvailabilityZone().getId()));
301
+ instance.getInstanceType().name(), instance.getPrivateAddress().getHostAddress(),
302
+ instance.getPublicAddress().getHostAddress(), region, instance.getLaunchTimePoint(),
303
+ instance.isSharedHost(), instance.getNameTag(), instance.getImageId(),
304
+ extractHealth(healths, instance), isDisposable, new AvailabilityZoneDTO(instance.getAvailabilityZone().getName(), instance.getRegion().getId(), instance.getAvailabilityZone().getId()));
302 305
}
303 306
304 307
@Override
... ...
@@ -377,9 +380,13 @@ public class LandscapeManagementWriteServiceImpl extends ResultCachingProxiedRem
377 380
}
378 381
379 382
private AwsInstanceDTO convertToAwsInstanceDTO(Host host) {
380
- return new AwsInstanceDTO(host.getId().toString(), host.getPrivateAddress().getHostAddress(), host.getPublicAddress() == null ? null : host.getPublicAddress().getHostAddress(),
381
- host.getRegion().getId(),
382
- host.getLaunchTimePoint(), host.isSharedHost(), new AvailabilityZoneDTO(host.getAvailabilityZone().getName(), host.getRegion().getId(), host.getAvailabilityZone().getId()));
383
+ return new AwsInstanceDTO(host.getId().toString(),
384
+ (host instanceof AwsInstance<?>) ? ((AwsInstance<?>) host).getInstanceType().name() : null,
385
+ host.getPrivateAddress().getHostAddress(),
386
+ host.getPublicAddress() == null ? null : host.getPublicAddress().getHostAddress(),
387
+ host.getRegion().getId(), host.getLaunchTimePoint(), host.isSharedHost(),
388
+ new AvailabilityZoneDTO(host.getAvailabilityZone().getName(), host.getRegion().getId(),
389
+ host.getAvailabilityZone().getId()));
383 390
}
384 391
385 392
@Override
... ...
@@ -676,6 +683,46 @@ public class LandscapeManagementWriteServiceImpl extends ResultCachingProxiedRem
676 683
optionalMemoryTotalSizeFactorOrNull, optionalIgtimiRiotPort, optionalPreferredInstanceToDeployUnmanagedReplicaTo);
677 684
}
678 685
686
+ @Override
687
+ public void createArchiveReplicaSet(String regionId, SailingApplicationReplicaSetDTO<String> archiveReplicaSetToUpgrade,
688
+ String instanceType, String releaseNameOrNullForLatestMaster, String optionalKeyName,
689
+ byte[] privateKeyEncryptionPassphrase, String securityReplicationBearerToken,
690
+ String replicaReplicationBearerToken, Integer optionalMemoryInMegabytesOrNull, Integer optionalMemoryTotalSizeFactorOrNull) throws Exception {
691
+ checkLandscapeManageAwsPermission();
692
+ final String userSetOrArchiveServerSecurityReplicationBearerToken;
693
+ final AwsRegion region = new AwsRegion(regionId, getLandscape());
694
+ final AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> awsReplicaSet =
695
+ convertFromApplicationReplicaSetDTO(region, archiveReplicaSetToUpgrade, optionalKeyName, privateKeyEncryptionPassphrase);
696
+ final SailingAnalyticsProcess<String> master = awsReplicaSet.getMaster();
697
+ if (Util.hasLength(securityReplicationBearerToken)) {
698
+ userSetOrArchiveServerSecurityReplicationBearerToken = securityReplicationBearerToken;
699
+ } else {
700
+ userSetOrArchiveServerSecurityReplicationBearerToken = master.getEnvShValueFor(
701
+ DefaultProcessConfigurationVariables.REPLICATE_MASTER_BEARER_TOKEN,
702
+ Landscape.WAIT_FOR_PROCESS_TIMEOUT, Optional.of(optionalKeyName), privateKeyEncryptionPassphrase);
703
+ }
704
+ final String replicaSetName = SharedLandscapeConstants.ARCHIVE_SERVER_APPLICATION_REPLICA_SET_NAME;
705
+ final String domainName = AwsLandscape.getHostedZoneName(archiveReplicaSetToUpgrade.getHostname());
706
+ final Database databaseConfiguration = master.getDatabaseConfiguration(region,
707
+ Landscape.WAIT_FOR_PROCESS_TIMEOUT, Optional.ofNullable(optionalKeyName),
708
+ privateKeyEncryptionPassphrase);
709
+ final URL requestURL = new URL(getThreadLocalRequest().getRequestURL().toString());
710
+ final URL continuationBaseURL = new URL(requestURL.getProtocol(), requestURL.getHost(), requestURL.getPort(), "/");
711
+ getLandscapeService()
712
+ .createArchiveReplicaSet(regionId, replicaSetName, instanceType, releaseNameOrNullForLatestMaster,
713
+ databaseConfiguration, optionalKeyName, privateKeyEncryptionPassphrase,
714
+ replicaReplicationBearerToken, domainName, optionalMemoryInMegabytesOrNull,
715
+ userSetOrArchiveServerSecurityReplicationBearerToken, optionalMemoryTotalSizeFactorOrNull,
716
+ /* optionalIgtimiRiotPort */ null, continuationBaseURL);
717
+ }
718
+
719
+ @Override
720
+ public void makeCandidateArchiveServerGoLive(String regionId, SailingApplicationReplicaSetDTO<String> archiveReplicaSetToUpgrade,
721
+ String optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception {
722
+ final String domainName = AwsLandscape.getHostedZoneName(archiveReplicaSetToUpgrade.getHostname());
723
+ getLandscapeService().makeCandidateArchiveServerGoLive(regionId, optionalKeyName, privateKeyEncryptionPassphrase, domainName);
724
+ }
725
+
679 726
/**
680 727
* Starts a first master process of a new replica set whose name is provided by the {@code replicaSetName}
681 728
* parameter. The process is started on the host identified by the {@code hostToDeployTo} parameter. A set of
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/shared/AwsInstanceDTO.java
... ...
@@ -5,6 +5,7 @@ import com.sap.sse.common.TimePoint;
5 5
6 6
public class AwsInstanceDTO implements IsSerializable {
7 7
private String instanceId;
8
+ private String instanceType;
8 9
private AvailabilityZoneDTO availabilityZoneDTO;
9 10
private String privateIpAddress;
10 11
private String publicIpAddress;
... ...
@@ -14,9 +15,10 @@ public class AwsInstanceDTO implements IsSerializable {
14 15
@Deprecated
15 16
AwsInstanceDTO() {} // for GWT RPC serialization only
16 17
17
- public AwsInstanceDTO(String instanceId, String privateIpAddress, String publicIpAddress, String region, TimePoint launchTimePoint, boolean shared, AvailabilityZoneDTO azDTO) {
18
+ public AwsInstanceDTO(String instanceId, String instanceType, String privateIpAddress, String publicIpAddress, String region, TimePoint launchTimePoint, boolean shared, AvailabilityZoneDTO azDTO) {
18 19
super();
19 20
this.instanceId = instanceId;
21
+ this.instanceType = instanceType;
20 22
this.availabilityZoneDTO = azDTO;
21 23
this.privateIpAddress = privateIpAddress;
22 24
this.publicIpAddress = publicIpAddress;
... ...
@@ -27,10 +29,12 @@ public class AwsInstanceDTO implements IsSerializable {
27 29
public String getAvailabilityZoneId() {
28 30
return availabilityZoneDTO.getAzId();
29 31
}
32
+ public String getInstanceType() {
33
+ return instanceType;
34
+ }
30 35
public String getInstanceId() {
31 36
return instanceId;
32 37
}
33
-
34 38
public AvailabilityZoneDTO getAvailabilityZoneDTO() {
35 39
return availabilityZoneDTO;
36 40
}
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/shared/ReverseProxyDTO.java
... ...
@@ -14,10 +14,10 @@ public class ReverseProxyDTO extends AwsInstanceDTO implements Named {
14 14
private String health;
15 15
private boolean isDisposable = false;
16 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);
17
+ public ReverseProxyDTO(String instanceId, String instanceType, String privateIpAddress,
18
+ String publicIpAddress, String region, TimePoint launchTimePoint, boolean shared, String name, String imageId,
19
+ String healthInTargetGroup, boolean isDisposable, AvailabilityZoneDTO availabilityZoneDTO) {
20
+ super(instanceId, instanceType, privateIpAddress, publicIpAddress, region, launchTimePoint, shared, availabilityZoneDTO);
21 21
this.name = name;
22 22
this.amiId = imageId;
23 23
this.health = healthInTargetGroup;
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/shared/SailingApplicationReplicaSetDTO.java
... ...
@@ -18,6 +18,9 @@ public class SailingApplicationReplicaSetDTO<ShardingKey> implements Named, IsSe
18 18
private String version;
19 19
private String releaseNotesLink;
20 20
private String hostname;
21
+ /**
22
+ * Originates from load balancer rule and therefore it can be null of not managed by load balancer rule
23
+ */
21 24
private String defaultRedirectPath;
22 25
private String autoScalingGroupAmiId;
23 26
... ...
@@ -74,6 +77,9 @@ public class SailingApplicationReplicaSetDTO<ShardingKey> implements Named, IsSe
74 77
return hostname;
75 78
}
76 79
80
+ /**
81
+ * @return may be null if not managed by load balancer rule
82
+ */
77 83
public String getDefaultRedirectPath() {
78 84
return defaultRedirectPath;
79 85
}
java/com.sap.sailing.landscape/resources/stringmessages/SailingLandscape_StringMessages.properties
... ...
@@ -1,12 +1,20 @@
1 1
MasterUnavailableMailSubject=Primary server of replica set {0} temporarily unavailable
2
-MasterUnavailableMailBody=The primary server of replica set {0} is temporarily unavailable.\nModifying access will be suspended.\nModifications incurred by a replica will be queued and will be applied when the primary is available again.\n\nYou are receiving this mail because you have administrative permissions for {0}.\nRemove those permissions at https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace: if you do not like to receive these messages anymore.
2
+MasterUnavailableMailBody=The primary server of replica set {0} is temporarily unavailable.\nModifying access will be suspended.\nModifications incurred by a replica will be queued and will be applied when the primary is available again.\n\nYou are receiving this mail because you have administrative permissions for {0}.\nRemove those permissions at <https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:> if you do not like to receive these messages anymore.
3 3
MasterAvailableMailSubject=Primary server of replica set {0} available again
4 4
MasterAvailableMailBody=The primary server of replica set {0} is available again.\nModifying access is possible again.\nQueued modifications will now process.\n\nYou are receiving this mail because you have administrative permissions for {0}.\nRemove those permissions at https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace: if you do not like to receive these messages anymore.
5
+StartingNewArchiveCandidateSubject=Starting new {0} candidate
6
+StartingNewArchiveCandidateBody=Starting new {0} candidate.\nFirst, archived content is loaded from the MongoDB; afterwards wind estimations and mark passings are calculated.\nThe whole process can take up to two days.\nYou will receive another e-mail when this phase has finished.\nAlso check the status of the candidate at https://archive-canidate.sapsailing.com/gwt/status
7
+NewArchiveServerLiveSubject=New {0} server is live
8
+NewArchiveServerLiveBody=New {0} server is live.\nYou can now archive content to {0}.\n\nYou are receiving this mail because you have administrative permissions for {0}.\nRemove those permissions at <https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:> if you do not like to receive these messages anymore.
5 9
StartingToArchiveReplicaSetIntoSubject=Starting to archive a replica set to {0}
6
-StartingToArchiveReplicaSetIntoBody=Starting to archive a replica set to {0}.\nWhile this is going on, refrain from archiving another replica set into the same archive {0}.\nAfter the content has been imported it will be compared to the original.\nYou should receive another e-mail when this has completed.\n\nYou are receiving this mail because you have administrative permissions for {0}.\nRemove those permissions at https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace: if you do not like to receive these messages anymore.
10
+StartingToArchiveReplicaSetIntoBody=Starting to archive a replica set to {0}.\nWhile this is going on, refrain from archiving another replica set into the same archive {0}.\nAfter the content has been imported it will be compared to the original.\nYou should receive another e-mail when this has completed.\n\nYou are receiving this mail because you have administrative permissions for {0}.\nRemove those permissions at <https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:> if you do not like to receive these messages anymore.
7 11
StartingToArchiveReplicaSetSubject=Starting to archive replica set {0}
8
-StartingToArchiveReplicaSetBody=Starting to archive a replica set {0}.\nWhile this is ongoing please do not make any modifications to {0}.\nYou will receive another e-mail when this process has finished.\n\nYou are receiving this mail because you have administrative permissions for {0}.\nRemove those permissions at https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace: if you do not like to receive these messages anymore.
12
+StartingToArchiveReplicaSetBody=Starting to archive a replica set {0}.\nWhile this is ongoing please do not make any modifications to {0}.\nYou will receive another e-mail when this process has finished.\n\nYou are receiving this mail because you have administrative permissions for {0}.\nRemove those permissions at <https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:> if you do not like to receive these messages anymore.
9 13
FinishedToArchiveReplicaSetIntoSubject=Archiving a replica set to {0} finished
10
-FinishedToArchiveReplicaSetIntoBody=Archiving a replica set to {0} has finished.\nYou can now archive other content to {0} if you want.\n\nYou are receiving this mail because you have administrative permissions for {0}.\nRemove those permissions at https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace: if you do not like to receive these messages anymore.
14
+FinishedToArchiveReplicaSetIntoBody=Archiving a replica set to {0} has finished.\nYou can now archive other content to {0} if you want.\n\nYou are receiving this mail because you have administrative permissions for {0}.\nRemove those permissions at <https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:> if you do not like to receive these messages anymore.
11 15
FinishedToArchiveReplicaSetSubject=Archiving replica set {0} finished
12
-FinishedToArchiveReplicaSetBody=Archiving replica set {0} has finished.\nIf you requested so, the original replica set has been removed.\n\nYou are receiving this mail because you have administrative permissions for {0}.\nRemove those permissions at https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace: if you do not like to receive these messages anymore.
... ...
\ No newline at end of file
0
+FinishedToArchiveReplicaSetBody=Archiving replica set {0} has finished.\nIf you requested so, the original replica set has been removed.\n\nYou are receiving this mail because you have administrative permissions for {0}.\nRemove those permissions at <https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:> if you do not like to receive these messages anymore.
1
+NewArchiveCandidateReadyForSpotChecksAndRotationSubject=The new {0} candidate is ready for spot checks
2
+NewArchiveCandidateReadyForSpotChecksAndRotationBody=We''ve run the following checks:\n{3}.\nThe new {0} candidate is ready for spot checks and, if OK, rotation to become the new production {0}.\nRun your spot checks at <https://{1}/gwt/Home.html#EventsPlace:> and compare to <https://{2}/gwt/Home.html#EventsPlace:>.\nStart the rotation to the new production server at <{4}/gwt/AdminConsole.html#LandscapeManagementPlace:> after successful checks.
3
+NewArchiveCandidateFailedSubject=Check {1} failed during {0} startup
4
+NewArchiveCandidateFailedBody=The check "{1}" failed with message "{2}" while starting up {0}. Please check the landscape and fix manually.
... ...
\ No newline at end of file
java/com.sap.sailing.landscape/resources/stringmessages/SailingLandscape_StringMessages_de.properties
... ...
@@ -1,12 +1,20 @@
1 1
MasterUnavailableMailSubject=Primär-Server des Anwendungs-Clusters {0} vorübergehend nicht verfügbar
2
-MasterUnavailableMailBody=Der Primär-Server des Anwendungs-Clusters {0} ist vorübergehend nicht verfügbar.\nVerändernde Zugriffe sind derzeit nicht möglich.\nModifikationen, die durch eine Replika ausgeüfhrt werden, werden gepuffert und werden angewandt, wenn der primäre Server wieder verfügbar ist.\n\nDiese Nachricht wurde versandt, weil Du über administrative Rechte für {0} verfügst.\nUm das zu ändern, besuche https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:, um diese Rechte für Dein Benutzerkonto zu entfernen.
2
+MasterUnavailableMailBody=Der Primär-Server des Anwendungs-Clusters {0} ist vorübergehend nicht verfügbar.\nVerändernde Zugriffe sind derzeit nicht möglich.\nModifikationen, die durch eine Replika ausgeüfhrt werden, werden gepuffert und werden angewandt, wenn der primäre Server wieder verfügbar ist.\n\nDiese Nachricht wurde versandt, weil Du über administrative Rechte für {0} verfügst.\nUm das zu ändern, besuche <https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:>, um diese Rechte für Dein Benutzerkonto zu entfernen.
3 3
MasterAvailableMailSubject=Primär-Server des Anwendungs-Clusters {0} wieder verfügbar
4
-MasterAvailableMailBody=Der Primär-Server des Anwendungs-Clusters {0} ist wieder verfügbar.\nVerändernde Zugriffe sind jetzt wieder möglich.\nGepufferte Modifikationen werden jetzt abgearbeitet.\n\nDiese Nachricht wurde versandt, weil Du über administrative Rechte für {0} verfügst.\nUm das zu ändern, besuche https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:, um diese Rechte für Dein Benutzerkonto zu entfernen.
4
+MasterAvailableMailBody=Der Primär-Server des Anwendungs-Clusters {0} ist wieder verfügbar.\nVerändernde Zugriffe sind jetzt wieder möglich.\nGepufferte Modifikationen werden jetzt abgearbeitet.\n\nDiese Nachricht wurde versandt, weil Du über administrative Rechte für {0} verfügst.\nUm das zu ändern, besuche <https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:>, um diese Rechte für Dein Benutzerkonto zu entfernen.
5
+StartingNewArchiveCandidateSubject=Neuer {0}-Kandidat wird gestartet
6
+StartingNewArchiveCandidateBody=Neuer {0}-Kandidat wird gestartet.\nZuerst werden dabei die archivierten Veranstaltungen von der Datenbank geladen. Danach werden Manöver, Windschätzungen und Zwischenzeiten berechnet.\nDieser Prozess kann bis zu zwei vollen Tagen dauern.\nEine weitere e-Mail benachrichtigt über den Abschluss dieser Phase.\nUnter https://archive-canidate.sapsailing.com/gwt/status kann der Status des Kandidaten verfolgt werden.
7
+NewArchiveServerLiveSubject=Neuer {0}-Server ist live
8
+NewArchiveServerLiveBody=Neuer {0}-Server ist live.\nAb jetzt dürfen wieder Anwendungs-Cluster nach {0} archiviert werden.\n\nDiese Nachricht wurde versandt, weil Du über administrative Rechte für {0} verfügst.\nUm das zu ändern, besuche https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:, um diese Rechte für Dein Benutzerkonto zu entfernen.
5 9
StartingToArchiveReplicaSetIntoSubject=Anwendungs-Cluster wird nach {0} archiviert
6
-StartingToArchiveReplicaSetIntoBody=Ein Anwendungs-Cluster wird nach {0} archiviert.\nWährend dieser Vorgang läuft, dürfen keine weitere Anwendungs-Cluster nach {0} archiviert werden.\nNach der Archivierung werden die Inhalte verglichen.\nEine e-Mail folgt, wenn der Vorgang abgeschlossen ist.\n\nDiese Nachricht wurde versandt, weil Du über administrative Rechte für {0} verfügst.\nUm das zu ändern, besuche https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:, um diese Rechte für Dein Benutzerkonto zu entfernen.
10
+StartingToArchiveReplicaSetIntoBody=Ein Anwendungs-Cluster wird nach {0} archiviert.\nWährend dieser Vorgang läuft, dürfen keine weitere Anwendungs-Cluster nach {0} archiviert werden.\nNach der Archivierung werden die Inhalte verglichen.\nEine e-Mail folgt, wenn der Vorgang abgeschlossen ist.\n\nDiese Nachricht wurde versandt, weil Du über administrative Rechte für {0} verfügst.\nUm das zu ändern, besuche <https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:>, um diese Rechte für Dein Benutzerkonto zu entfernen.
7 11
StartingToArchiveReplicaSetSubject=Anwendungs-Cluster {0} wird archiviert
8
-StartingToArchiveReplicaSetBody=Das Anwendungs-Cluster {0} wird jetzt archiviert.\nWährenddessen dürfen keine Veränderungen an {0} vorgenommen werden.\nEine e-Mail folgt, wenn der Vorgang abgeschlossen ist.\n\nDiese Nachricht wurde versandt, weil Du über administrative Rechte für {0} verfügst.\nUm das zu ändern, besuche https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:, um diese Rechte für Dein Benutzerkonto zu entfernen.
12
+StartingToArchiveReplicaSetBody=Das Anwendungs-Cluster {0} wird jetzt archiviert.\nWährenddessen dürfen keine Veränderungen an {0} vorgenommen werden.\nEine e-Mail folgt, wenn der Vorgang abgeschlossen ist.\n\nDiese Nachricht wurde versandt, weil Du über administrative Rechte für {0} verfügst.\nUm das zu ändern, besuche <https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:>, um diese Rechte für Dein Benutzerkonto zu entfernen.
9 13
FinishedToArchiveReplicaSetIntoSubject=Archivierung des Anwendungs-Clusters nach {0} beendet
10
-FinishedToArchiveReplicaSetIntoBody=Die Archivierung des Anwendungs-Clusters nach {0} ist beendet.\nAb jetzt sind bei Bedarf weitere Archivierungen nach {0} möglich.\n\nDiese Nachricht wurde versandt, weil Du über administrative Rechte für {0} verfügst.\nUm das zu ändern, besuche https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:, um diese Rechte für Dein Benutzerkonto zu entfernen.
14
+FinishedToArchiveReplicaSetIntoBody=Die Archivierung des Anwendungs-Clusters nach {0} ist beendet.\nAb jetzt sind bei Bedarf weitere Archivierungen nach {0} möglich.\n\nDiese Nachricht wurde versandt, weil Du über administrative Rechte für {0} verfügst.\nUm das zu ändern, besuche <https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:>, um diese Rechte für Dein Benutzerkonto zu entfernen.
11 15
FinishedToArchiveReplicaSetSubject=Archivierung des Anwendungs-Clusters {0} beendet
12
-FinishedToArchiveReplicaSetBody=Die Archivierung des Anwendungs-Clusters {0} ist beendet.\nFalls angefragt, wurde das Original Anwendungs-Cluster entfernt.\n\nDiese Nachricht wurde versandt, weil Du über administrative Rechte für {0} verfügst.\nUm das zu ändern, besuche https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:, um diese Rechte für Dein Benutzerkonto zu entfernen.
... ...
\ No newline at end of file
0
+FinishedToArchiveReplicaSetBody=Die Archivierung des Anwendungs-Clusters {0} ist beendet.\nFalls angefragt, wurde das Original Anwendungs-Cluster entfernt.\n\nDiese Nachricht wurde versandt, weil Du über administrative Rechte für {0} verfügst.\nUm das zu ändern, besuche <https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:>, um diese Rechte für Dein Benutzerkonto zu entfernen.
1
+NewArchiveCandidateReadyForSpotChecksAndRotationSubject=Der neue {0} Kandidat ist bereit für einen stichprobenartigen Vergleich
2
+NewArchiveCandidateReadyForSpotChecksAndRotationBody=Es wurden die folgenden Prüfungen durchgeführt:\n{3}.\nDer neue {0} Kandidat ist bereit für einen stichprobenartigen Vergleich\nund, falls OK, Rotation zum neuen Produktiv-Server für {0}.\nStichprobenartiger Vergleich unter https://{1}/gwt/Home.html#EventsPlace: und https://{2}/gwt/Home.html#EventsPlace:.\nRotation nach erfolgreichen Prüfungen unter <{4}/gwt/AdminConsole.html#LandscapeManagementPlace:> starten.
3
+NewArchiveCandidateFailedSubject=Überprüfung {1} beim Start von {0} fehlgeschlagen
4
+NewArchiveCandidateFailedBody=Die Überprüfung "{1}" zum Start von {0} ist mit der Meldung "{2}" fehlgeschlagen. Bitte die Landschaft inspizieren und manuell korrigieren.
... ...
\ No newline at end of file
java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/LandscapeService.java
... ...
@@ -2,6 +2,7 @@ package com.sap.sailing.landscape;
2 2
3 3
import java.io.IOException;
4 4
import java.net.MalformedURLException;
5
+import java.net.URL;
5 6
import java.util.Map;
6 7
import java.util.Optional;
7 8
import java.util.concurrent.ExecutionException;
... ...
@@ -17,8 +18,10 @@ import com.sap.sailing.landscape.procedures.SailingProcessConfigurationVariables
17 18
import com.sap.sailing.landscape.procedures.StartMultiServer;
18 19
import com.sap.sailing.server.gateway.interfaces.CompareServersResult;
19 20
import com.sap.sailing.server.gateway.interfaces.SailingServer;
21
+import com.sap.sailing.server.gateway.interfaces.SailingServerFactory;
20 22
import com.sap.sse.common.Duration;
21 23
import com.sap.sse.common.Util.Triple;
24
+import com.sap.sse.common.mail.MailException;
22 25
import com.sap.sse.landscape.Release;
23 26
import com.sap.sse.landscape.application.ApplicationReplicaSet;
24 27
import com.sap.sse.landscape.aws.AmazonMachineImage;
... ...
@@ -26,8 +29,11 @@ import com.sap.sse.landscape.aws.AwsApplicationReplicaSet;
26 29
import com.sap.sse.landscape.aws.AwsAvailabilityZone;
27 30
import com.sap.sse.landscape.aws.AwsLandscape;
28 31
import com.sap.sse.landscape.aws.impl.AwsRegion;
32
+import com.sap.sse.landscape.mongodb.Database;
29 33
import com.sap.sse.landscape.mongodb.MongoEndpoint;
30 34
import com.sap.sse.security.SecurityService;
35
+import com.sap.sse.security.shared.HasPermissions.Action;
36
+import com.sap.sse.security.shared.impl.User;
31 37
32 38
import software.amazon.awssdk.services.autoscaling.model.AutoScalingGroup;
33 39
import software.amazon.awssdk.services.ec2.model.InstanceType;
... ...
@@ -151,6 +157,35 @@ public interface LandscapeService {
151 157
Integer optionalMemoryInMegabytesOrNull, Integer optionalMemoryTotalSizeFactorOrNull,
152 158
Integer optionalIgtimiRiotPort, Optional<Integer> minimumAutoScalingGroupSize, Optional<Integer> maximumAutoScalingGroupSize)
153 159
throws Exception;
160
+
161
+ /**
162
+ * Runs phase 1 of an ARCHIVE server upgrade. This includes launching the new instance in a favorable availability
163
+ * zone where ideally we have a reverse proxy and that ideally is different from the AZ in which the current
164
+ * production ARCHIVE server runs. It then installs a {@link ArchiveCandidateMonitoringBackgroundTask background
165
+ * task} that keeps applying a sequence of checks. When any of the checks keeps failing beyond a timeout, the
166
+ * activity is aborted, and the user who triggered it receives an e-mail about this. If all checks pass, the user
167
+ * receives an e-mail that asks for manual spot checks and a confirmation about the rotation. A link embedded in the
168
+ * e-mail grants the user easy access to the
169
+ * {@link #makeCandidateArchiveServerGoLive(String, String, byte[], String)} method which then performs phase 2.
170
+ *
171
+ * @param continuationBaseURL
172
+ * the base URL to which to direct the user for continuation of the ARCHIVE upgrade process (phase 2)
173
+ * after this first phase has completed successfully
174
+ */
175
+ void createArchiveReplicaSet(
176
+ String regionId, String name, String instanceType, String releaseNameOrNullForLatestMaster, Database databaseConfiguration,
177
+ String optionalKeyName, byte[] privateKeyEncryptionPassphrase, String replicaReplicationBearerToken,
178
+ String optionalDomainName, Integer optionalMemoryInMegabytesOrNull, String securityServiceReplicationBearerToken,
179
+ Integer optionalMemoryTotalSizeFactorOrNull, Integer optionalIgtimiRiotPort, URL continuationBaseURL) throws Exception;
180
+
181
+ /**
182
+ * Phase 2 of an ARCHIVE server upgrade. This is to be triggered ideally after a "human in the loop" step
183
+ * where a user makes some spot checks and then confirms that the archive candidate can be installed as the
184
+ * new production server, with the previous production server then becoming the failover, and the old failover
185
+ * instance being terminated.
186
+ */
187
+ void makeCandidateArchiveServerGoLive(String regionId, String optionalKeyName,
188
+ byte[] privateKeyEncryptionPassphrase, String optionalDomainName) throws Exception;
154 189
155 190
/**
156 191
* Starts a first master process of a new replica set whose name is provided by the {@code replicaSetName}
... ...
@@ -485,4 +520,27 @@ public interface LandscapeService {
485 520
String optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception;
486 521
487 522
String getHostname(String replicaSetName, String optionalDomainName);
523
+
524
+ /**
525
+ * @param subjectMessageKey
526
+ * must have a single placeholder argument representing the name of the replica set
527
+ * @param bodyMessageKey
528
+ * must have a single placeholder argument representing the name of the replica set
529
+ * @param alsoSendToAllUsersWithThisPermissionOnReplicaSet
530
+ * when not empty, all users that have permission to this {@link SecuredSecurityTypes#SERVER SERVER}
531
+ * action on the {@code replicaSet} will receive the e-mail in addition to the server owner. No user
532
+ * will receive the e-mail twice.
533
+ */
534
+ void sendMailToReplicaSetOwner(
535
+ AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet,
536
+ String subjectMessageKey, String bodyMessageKey,
537
+ Optional<Action> alsoSendToAllUsersWithThisPermissionOnReplicaSet) throws MailException;
538
+
539
+ void sendMailToCurrentUser(String messageSubjectKey, String messageBodyKey, String... messageParameters)
540
+ throws MailException;
541
+
542
+ void sendMailToUser(User user, String messageSubjectKey, String messageBodyKey, String... messageParameters)
543
+ throws MailException;
544
+
545
+ SailingServerFactory getSailingServerFactory();
488 546
}
java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/SailingAnalyticsProcess.java
... ...
@@ -2,6 +2,7 @@ package com.sap.sailing.landscape;
2 2
3 3
import java.io.IOException;
4 4
import java.util.Optional;
5
+import java.util.concurrent.TimeoutException;
5 6
import java.util.logging.Logger;
6 7
7 8
import com.jcraft.jsch.JSchException;
... ...
@@ -9,6 +10,7 @@ import com.sap.sailing.landscape.procedures.SailingProcessConfigurationVariables
9 10
import com.sap.sse.common.Duration;
10 11
import com.sap.sse.landscape.Release;
11 12
import com.sap.sse.landscape.aws.AwsApplicationProcess;
13
+import com.sap.sse.util.ThreadPoolUtil;
12 14
13 15
public interface SailingAnalyticsProcess<ShardingKey> extends AwsApplicationProcess<ShardingKey, SailingAnalyticsMetrics, SailingAnalyticsProcess<ShardingKey>> {
14 16
static Logger logger = Logger.getLogger(SailingAnalyticsProcess.class.getName());
... ...
@@ -36,4 +38,42 @@ public interface SailingAnalyticsProcess<ShardingKey> extends AwsApplicationProc
36 38
*/
37 39
void refreshToRelease(Release release, Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase)
38 40
throws IOException, InterruptedException, JSchException, Exception;
41
+
42
+ double getLastMinuteSystemLoadAverage(Optional<Duration> optionalTimeout) throws TimeoutException, Exception;
43
+
44
+ /**
45
+ * The count of <em>all</em> tasks in the default background thread pool executor's queue, including those scheduled
46
+ * with a delay, such as rate limiter and cache clean-up tasks or periodic fetching of remote server content.
47
+ *
48
+ * @see ThreadPoolUtil#getDefaultBackgroundTaskThreadPoolExecutor()
49
+ */
50
+ int getDefaultBackgroundThreadPoolExecutorQueueSize(Optional<Duration> optionalTimeout) throws TimeoutException, Exception;
51
+
52
+ /**
53
+ * The count of <em>all</em> tasks in the default foreground thread pool executor's queue, including those scheduled
54
+ * with a delay, such as rate limiter and cache clean-up tasks or periodic fetching of remote server content.
55
+ *
56
+ * @see ThreadPoolUtil#getDefaultForegroundTaskThreadPoolExecutor()
57
+ */
58
+ int getDefaultForegroundThreadPoolExecutorQueueSize(Optional<Duration> optionalTimeout) throws TimeoutException, Exception;
59
+
60
+ /**
61
+ * The count of only those tasks in the default background thread pool executor's queue that have no scheduling
62
+ * delay or a delay below {@link Duration#ONE_SECOND one second}. This helps exclude from the count those rate
63
+ * limiter and cache clean-up tasks or periodic fetching of remote server content. It is as such as good approximation
64
+ * of the "immediate" tasks that will keep the server busy in the immediate future.
65
+ *
66
+ * @see ThreadPoolUtil#getDefaultBackgroundTaskThreadPoolExecutor()
67
+ */
68
+ int getDefaultBackgroundThreadPoolExecutorQueueSizeNondelayed(Optional<Duration> optionalTimeout) throws TimeoutException, Exception;
69
+
70
+ /**
71
+ * The count of only those tasks in the default foreground thread pool executor's queue that have no scheduling
72
+ * delay or a delay below {@link Duration#ONE_SECOND one second}. This helps exclude from the count those rate
73
+ * limiter and cache clean-up tasks or periodic fetching of remote server content. It is as such as good approximation
74
+ * of the "immediate" tasks that will keep the server busy in the immediate future.
75
+ *
76
+ * @see ThreadPoolUtil#getDefaultForegroundTaskThreadPoolExecutor()
77
+ */
78
+ int getDefaultForegroundThreadPoolExecutorQueueSizeNondelayed(Optional<Duration> optionalTimeout) throws TimeoutException, Exception;
39 79
}
java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/impl/ArchiveCandidateMonitoringBackgroundTask.java
... ...
@@ -0,0 +1,327 @@
1
+package com.sap.sailing.landscape.impl;
2
+
3
+import java.net.URL;
4
+import java.util.Arrays;
5
+import java.util.Iterator;
6
+import java.util.Optional;
7
+import java.util.concurrent.ExecutionException;
8
+import java.util.concurrent.ScheduledExecutorService;
9
+import java.util.concurrent.TimeUnit;
10
+import java.util.logging.Logger;
11
+
12
+import com.sap.sailing.landscape.LandscapeService;
13
+import com.sap.sailing.landscape.SailingAnalyticsMetrics;
14
+import com.sap.sailing.landscape.SailingAnalyticsProcess;
15
+import com.sap.sailing.server.gateway.interfaces.CompareServersResult;
16
+import com.sap.sailing.server.gateway.interfaces.SailingServer;
17
+import com.sap.sse.common.Duration;
18
+import com.sap.sse.common.Named;
19
+import com.sap.sse.common.TimePoint;
20
+import com.sap.sse.common.Util;
21
+import com.sap.sse.common.impl.NamedImpl;
22
+import com.sap.sse.common.mail.MailException;
23
+import com.sap.sse.landscape.Landscape;
24
+import com.sap.sse.landscape.aws.AwsApplicationReplicaSet;
25
+import com.sap.sse.security.shared.impl.User;
26
+
27
+/**
28
+ * A stateful monitoring task that can be {@link #run run} to observe an {@code ARCHIVE} candidate process and wait for
29
+ * it to be ready for comparing its contents with a production {@code ARCHIVE} instance. When run for the first time,
30
+ * the task check the {@code /gwt/status} end point on {@code candidateHostname} to see whether it is already serving
31
+ * requests. If not, it will re-schedule itself after some delay to check again until either the candidate becomes
32
+ * healthy or a timeout is reached.
33
+ * <p>
34
+ *
35
+ * If the candidate was seen serving a {@code /gwt/status} request, this task changes state and now looks at the
36
+ * contents of the status response. Four conditions must be fulfilled for the candidate to be considered ready for
37
+ * comparison:
38
+ *
39
+ * <ol>
40
+ * <li>the overall status must be {@code available: true}.</li>
41
+ * <li>the one-minute system load average must be below 2 (per cent)</li>
42
+ * <li>the default foreground thread pool queue must contain less than 10 tasks</li>
43
+ * <li>the default background thread pool queue must contain less than 10 tasks</li>
44
+ * <li>the old and new ARCHIVE must compare equal with the {@link SailingServer#compareServers(Optional, SailingServer, Optional)} method</li>
45
+ * </ol>
46
+ *
47
+ * When any of these conditions is not fulfilled, the task will re-schedule itself after some delay to check again until
48
+ * either the candidate fulfills all conditions or a timeout is reached.
49
+ * <p>
50
+ *
51
+ * @author Axel Uhl (d043530)
52
+ *
53
+ */
54
+public class ArchiveCandidateMonitoringBackgroundTask implements Runnable {
55
+ private interface Check extends Named {
56
+ boolean runCheck() throws Exception;
57
+ void setLastFailureMessage(String lastFailureMessage);
58
+ boolean hasTimedOut();
59
+ Duration getDelayAfterFailure();
60
+ String getLastFailureMessage();
61
+ }
62
+
63
+ private abstract class AbstractCheck extends NamedImpl implements Check {
64
+ private static final long serialVersionUID = -8809199091635882129L;
65
+ private final TimePoint creationTime;
66
+ private final Duration timeout;
67
+ private final Duration delayAfterFailure;
68
+ private String lastFailureMessage;
69
+
70
+ public AbstractCheck(String name, Duration timeout, Duration delayAfterFailure) {
71
+ super(name);
72
+ this.creationTime = TimePoint.now();
73
+ this.timeout = timeout;
74
+ this.delayAfterFailure = delayAfterFailure;
75
+ }
76
+
77
+ @Override
78
+ public boolean hasTimedOut() {
79
+ return creationTime.until(TimePoint.now()).compareTo(timeout) > 0;
80
+ }
81
+
82
+ @Override
83
+ public Duration getDelayAfterFailure() {
84
+ return delayAfterFailure;
85
+ }
86
+
87
+ @Override
88
+ public String getLastFailureMessage() {
89
+ return lastFailureMessage;
90
+ }
91
+
92
+ @Override
93
+ public void setLastFailureMessage(String lastFailureMessage) {
94
+ this.lastFailureMessage = lastFailureMessage;
95
+ }
96
+ }
97
+
98
+ private static final Logger logger = Logger.getLogger(ArchiveCandidateMonitoringBackgroundTask.class.getName());
99
+
100
+ private final static Duration DELAY_BETWEEN_CHECKS = Duration.ONE_MINUTE.times(5);
101
+ private final static Duration LONG_TIMEOUT = Duration.ONE_DAY.times(3);
102
+ private final static double MAXIMUM_ONE_MINUTE_SYSTEM_LOAD_AVERAGE = 2.0;
103
+ private final static int MAXIMUM_THREAD_POOL_QUEUE_SIZE = 10;
104
+ private final static Optional<Duration> TIMEOUT_FIRST_CONTACT = Optional.of(Landscape.WAIT_FOR_PROCESS_TIMEOUT.get().plus(Landscape.WAIT_FOR_HOST_TIMEOUT.get()));
105
+ private final static Duration SERVER_COMPARISON_TIMEOUT = Duration.ONE_MINUTE.times(10); // good for two or three attempts, usually
106
+ private final static Duration DELAY_BETWEEN_COMPARISON_CHECKS = Duration.ONE_MINUTE;
107
+
108
+ /**
109
+ * The user on whose behalf the monitoring is performed; this is used for sending notifications about the monitoring
110
+ * result to the user and for performing the monitoring with the same permissions as the user (e.g. when accessing
111
+ * the candidate's REST API)
112
+ */
113
+ private final User currentUser;
114
+ private final String candidateHostname;
115
+ private final LandscapeService landscapeService;
116
+ private final AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet;
117
+ private final URL continuationBaseURL;
118
+ private final ScheduledExecutorService executor;
119
+
120
+ /**
121
+ * A bearer token expected to authenticate the {@link #currentUser} against the candidate and production ARCHIVE
122
+ * servers
123
+ */
124
+ private final String effectiveBearerToken;
125
+
126
+ private Iterable<Check> checks;
127
+ private Iterator<Check> checksIterator;
128
+ private Check currentCheck;
129
+
130
+ public ArchiveCandidateMonitoringBackgroundTask(User currentUser, LandscapeService landscapeService,
131
+ AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet,
132
+ String candidateHostname,
133
+ ScheduledExecutorService executor,
134
+ String effectiveBearerToken, URL continuationBaseURL) {
135
+ this.currentUser = currentUser;
136
+ this.landscapeService = landscapeService;
137
+ this.continuationBaseURL = continuationBaseURL;
138
+ this.replicaSet = replicaSet;
139
+ this.candidateHostname = candidateHostname;
140
+ this.executor = executor;
141
+ this.effectiveBearerToken = effectiveBearerToken;
142
+ this.checks = Arrays.asList(
143
+ new IsAlive(),
144
+ new IsReady(),
145
+ new HasLowEnoughSystemLoad(),
146
+ new HasShortEnoughDefaultBackgroundThreadPoolExecutorQueue(),
147
+ new HasShortEnoughDefaultForegroundThreadPoolExecutorQueue(),
148
+ new CompareServersWithRestAPI());
149
+ this.checksIterator = this.checks.iterator();
150
+ this.currentCheck = checksIterator.next();
151
+ }
152
+
153
+ @Override
154
+ public void run() {
155
+ try {
156
+ if (currentCheck.runCheck()) {
157
+ logger.info("Check \""+currentCheck+"\" passed.");
158
+ // the check passed; proceed to next check, if any
159
+ currentCheck = checksIterator.hasNext() ? checksIterator.next() : null;
160
+ if (currentCheck != null) {
161
+ logger.info("More checks to do; re-scheduling to run next check \""+currentCheck+"\"");
162
+ // re-schedule this task to run next check immediately
163
+ executor.submit(this);
164
+ } else {
165
+ // all checks passed; candidate is ready for production; nothing more to do here
166
+ logger.info("Done with all checks; candidate is ready for production.");
167
+ notifyProcessOwnerCandidateIsReadyForSpotChecksAndRotation(); // this ends the re-scheduling loop
168
+ }
169
+ } else {
170
+ rescheduleCurrentCheckAfterFailureOrTimeout();
171
+ }
172
+ } catch (Exception e) {
173
+ logger.warning("Exception while running check \"" + currentCheck + "\" for candidate " + replicaSet.getMaster().getHost().getHostname() + ": " + e.getMessage());
174
+ currentCheck.setLastFailureMessage(e.getMessage());
175
+ try {
176
+ rescheduleCurrentCheckAfterFailureOrTimeout();
177
+ } catch (MailException e1) {
178
+ logger.severe("Issue while trying to send mail: "+e.getMessage()+"; user may not know what to do next!");
179
+ }
180
+ }
181
+ }
182
+
183
+ private void rescheduleCurrentCheckAfterFailureOrTimeout() throws MailException {
184
+ if (currentCheck.hasTimedOut()) {
185
+ logger.severe("Check \""+currentCheck+"\" failed and has timed out; giving up on candidate "+replicaSet.getMaster().getHost().getHostname());
186
+ notifyProcessOwnerCandidateFailedToBecomeReadyForProduction(); // this ends the re-scheduling loop
187
+ } else {
188
+ logger.info("Check \"" + currentCheck + "\" failed with message \"" + currentCheck.getLastFailureMessage()
189
+ + "\" but has not yet timed out; re-scheduling to check again after "
190
+ + currentCheck.getDelayAfterFailure());
191
+ executor.schedule(this, currentCheck.getDelayAfterFailure().asMillis(), TimeUnit.MILLISECONDS);
192
+ }
193
+ }
194
+
195
+ private class IsAlive extends AbstractCheck {
196
+ private static final long serialVersionUID = -4265303532881568290L;
197
+
198
+ private IsAlive() {
199
+ super("is alive", TIMEOUT_FIRST_CONTACT.get(), DELAY_BETWEEN_CHECKS);
200
+ }
201
+
202
+ @Override
203
+ public boolean runCheck() throws Exception {
204
+ final boolean result = replicaSet.getMaster().isAlive(Landscape.WAIT_FOR_PROCESS_TIMEOUT);
205
+ if (!result) {
206
+ setLastFailureMessage("Candidate at "+replicaSet.getMaster().getHost().getPrivateAddress()+" not alive yet");
207
+ }
208
+ return result;
209
+ }
210
+ }
211
+
212
+ private class IsReady extends AbstractCheck {
213
+ private static final long serialVersionUID = -4265303532881568290L;
214
+
215
+ private IsReady() {
216
+ super("is ready", LONG_TIMEOUT, DELAY_BETWEEN_CHECKS);
217
+ }
218
+
219
+ @Override
220
+ public boolean runCheck() throws Exception {
221
+ final boolean result = replicaSet.getMaster().isReady(Landscape.WAIT_FOR_PROCESS_TIMEOUT);
222
+ if (!result) {
223
+ setLastFailureMessage("Candidate at "+replicaSet.getMaster().getHost().getPrivateAddress()+" not ready yet");
224
+ }
225
+ return result;
226
+ }
227
+ }
228
+
229
+ private class HasLowEnoughSystemLoad extends AbstractCheck {
230
+ private static final long serialVersionUID = -7931266212387969287L;
231
+
232
+ public HasLowEnoughSystemLoad() {
233
+ super("has low enough system load", LONG_TIMEOUT, DELAY_BETWEEN_CHECKS);
234
+ }
235
+
236
+ @Override
237
+ public boolean runCheck() throws Exception {
238
+ final double lastMinuteSystemLoadAverage = replicaSet.getMaster().getLastMinuteSystemLoadAverage(Landscape.WAIT_FOR_PROCESS_TIMEOUT);
239
+ final boolean result = lastMinuteSystemLoadAverage < MAXIMUM_ONE_MINUTE_SYSTEM_LOAD_AVERAGE;
240
+ if (!result) {
241
+ setLastFailureMessage("Candidate at " + replicaSet.getMaster().getHost().getPrivateAddress()
242
+ + " has too high system load average of " + lastMinuteSystemLoadAverage
243
+ + " which is still above the maximum of " + MAXIMUM_ONE_MINUTE_SYSTEM_LOAD_AVERAGE);
244
+ }
245
+ return result;
246
+ }
247
+ }
248
+
249
+ private class HasShortEnoughDefaultBackgroundThreadPoolExecutorQueue extends AbstractCheck {
250
+ private static final long serialVersionUID = 3482148861663152178L;
251
+
252
+ public HasShortEnoughDefaultBackgroundThreadPoolExecutorQueue() {
253
+ super("has short enough default background thread pool executor queue", LONG_TIMEOUT, DELAY_BETWEEN_CHECKS);
254
+ }
255
+
256
+ @Override
257
+ public boolean runCheck() throws Exception {
258
+ final int defaultBackgroundThreadPoolExecutorQueueSize = replicaSet.getMaster().getDefaultBackgroundThreadPoolExecutorQueueSizeNondelayed(Landscape.WAIT_FOR_PROCESS_TIMEOUT);
259
+ final boolean result = defaultBackgroundThreadPoolExecutorQueueSize < MAXIMUM_THREAD_POOL_QUEUE_SIZE;
260
+ if (!result) {
261
+ setLastFailureMessage("Candidate at " + replicaSet.getMaster().getHost().getPrivateAddress()
262
+ + " has too many tasks in default background thread pool executor queue: "+defaultBackgroundThreadPoolExecutorQueueSize+
263
+ " which is still above the maximum of "+MAXIMUM_THREAD_POOL_QUEUE_SIZE);
264
+ }
265
+ return result;
266
+ }
267
+ }
268
+
269
+ private class HasShortEnoughDefaultForegroundThreadPoolExecutorQueue extends AbstractCheck {
270
+ private static final long serialVersionUID = 5194383164577435150L;
271
+
272
+ public HasShortEnoughDefaultForegroundThreadPoolExecutorQueue() {
273
+ super("has short enough default foreground thread pool executor queue", LONG_TIMEOUT, DELAY_BETWEEN_CHECKS);
274
+ }
275
+
276
+ @Override
277
+ public boolean runCheck() throws Exception {
278
+ final int defaultForegroundThreadPoolExecutorQueueSize = replicaSet.getMaster().getDefaultForegroundThreadPoolExecutorQueueSizeNondelayed(Landscape.WAIT_FOR_PROCESS_TIMEOUT);
279
+ final boolean result = defaultForegroundThreadPoolExecutorQueueSize < MAXIMUM_THREAD_POOL_QUEUE_SIZE;
280
+ if (!result) {
281
+ setLastFailureMessage("Candidate at "+replicaSet.getMaster().getHost().getPrivateAddress()
282
+ + " has too many tasks in default foreground thread pool executor queue: "+defaultForegroundThreadPoolExecutorQueueSize+
283
+ " which is still above the maximum of "+MAXIMUM_THREAD_POOL_QUEUE_SIZE);
284
+ }
285
+ return result;
286
+ }
287
+ }
288
+
289
+ private class CompareServersWithRestAPI extends AbstractCheck {
290
+ private static final long serialVersionUID = -5271988056894947109L;
291
+
292
+ public CompareServersWithRestAPI() {
293
+ super("compare servers with REST API", SERVER_COMPARISON_TIMEOUT, DELAY_BETWEEN_COMPARISON_CHECKS);
294
+ }
295
+
296
+ @Override
297
+ public boolean runCheck() throws Exception {
298
+ final SailingServer productionServer = landscapeService.getSailingServerFactory().getSailingServer(new URL("https", replicaSet.getHostname(), "/"), effectiveBearerToken);
299
+ final SailingServer candidateServer = landscapeService.getSailingServerFactory().getSailingServer(new URL("https", candidateHostname, "/"), effectiveBearerToken);
300
+ final CompareServersResult comparisonResult = candidateServer.compareServers(Optional.empty(), productionServer, Optional.empty());
301
+ if (comparisonResult.hasDiffs()) {
302
+ setLastFailureMessage(
303
+ "Candidate server does not match production server according to REST API comparison."
304
+ + "\nDifferences on candidate side: " + comparisonResult.getADiffs()
305
+ + "\nDifferences on production side: " + comparisonResult.getBDiffs()
306
+ + "\nNot proceeding further. You need to resolve the issues manually."
307
+ + "\nCheck https://"+candidateHostname+"/sailingserver/v1/compareservers?server2="+replicaSet.getHostname()
308
+ + "\nafter you have tried to resolve the differences."
309
+ + "\nThen, run your smoke checks and trigger the rotation if everything looks good.");
310
+ }
311
+ return !comparisonResult.hasDiffs();
312
+ }
313
+ }
314
+
315
+ private void notifyProcessOwnerCandidateFailedToBecomeReadyForProduction() throws MailException {
316
+ landscapeService.sendMailToUser(currentUser, "NewArchiveCandidateFailedSubject",
317
+ "NewArchiveCandidateFailedBody", replicaSet.getServerName(), currentCheck.getName(),
318
+ currentCheck.getLastFailureMessage());
319
+ }
320
+
321
+ private void notifyProcessOwnerCandidateIsReadyForSpotChecksAndRotation() throws MailException, InterruptedException, ExecutionException {
322
+ landscapeService.sendMailToUser(currentUser, "NewArchiveCandidateReadyForSpotChecksAndRotationSubject",
323
+ "NewArchiveCandidateReadyForSpotChecksAndRotationBody", replicaSet.getName(), candidateHostname,
324
+ replicaSet.getHostname(), " - "+Util.joinStrings("\n - ", Util.map(checks, Check::getName)),
325
+ continuationBaseURL.toString());
326
+ }
327
+}
java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/impl/LandscapeServiceImpl.java
... ...
@@ -18,6 +18,7 @@ import java.util.Set;
18 18
import java.util.UUID;
19 19
import java.util.concurrent.CompletableFuture;
20 20
import java.util.concurrent.ExecutionException;
21
+import java.util.concurrent.ScheduledExecutorService;
21 22
import java.util.concurrent.TimeoutException;
22 23
import java.util.function.Function;
23 24
import java.util.logging.Level;
... ...
@@ -83,6 +84,7 @@ import com.sap.sse.landscape.aws.AwsLandscape;
83 84
import com.sap.sse.landscape.aws.AwsShard;
84 85
import com.sap.sse.landscape.aws.HostSupplier;
85 86
import com.sap.sse.landscape.aws.ReverseProxy;
87
+import com.sap.sse.landscape.aws.ReverseProxyCluster;
86 88
import com.sap.sse.landscape.aws.Tags;
87 89
import com.sap.sse.landscape.aws.TargetGroup;
88 90
import com.sap.sse.landscape.aws.common.shared.RedirectDTO;
... ...
@@ -132,7 +134,7 @@ import software.amazon.awssdk.services.sts.model.Credentials;
132 134
public class LandscapeServiceImpl implements LandscapeService {
133 135
private static final Logger logger = Logger.getLogger(LandscapeServiceImpl.class.getName());
134 136
135
- private static final String STRING_MESSAGES_BASE_NAME = "stringmessages/SailingLandscape_StringMessages";
137
+ public static final String STRING_MESSAGES_BASE_NAME = "stringmessages/SailingLandscape_StringMessages";
136 138
137 139
private static final String TEMPORARY_UPGRADE_REPLICA_NAME_SUFFIX = " (Upgrade Replica)";
138 140
... ...
@@ -183,7 +185,7 @@ public class LandscapeServiceImpl implements LandscapeService {
183 185
newSharedMasterInstance ? optionalMemoryTotalSizeFactorOrNull : null, optionalIgtimiRiotPort, region, release);
184 186
final String bearerTokenUsedByReplicas = getEffectiveBearerToken(replicaReplicationBearerToken);
185 187
final InboundReplicationConfiguration inboundMasterReplicationConfiguration = masterConfigurationBuilder.getInboundReplicationConfiguration().get();
186
- establishServerGroupAndTryToMakeCurrentUserItsOwnerAndMember(name, bearerTokenUsedByReplicas,
188
+ establishServerAndServerGroupAndTryToMakeCurrentUserItsOwnerAndMember(name, bearerTokenUsedByReplicas,
187 189
inboundMasterReplicationConfiguration.getMasterHostname(), inboundMasterReplicationConfiguration.getMasterHttpPort());
188 190
final com.sap.sailing.landscape.procedures.StartSailingAnalyticsMasterHost.Builder<?, String> masterHostBuilder = StartSailingAnalyticsMasterHost.masterHostBuilder(masterConfigurationBuilder);
189 191
masterHostBuilder
... ...
@@ -236,6 +238,160 @@ public class LandscapeServiceImpl implements LandscapeService {
236 238
}
237 239
238 240
@Override
241
+ public void createArchiveReplicaSet(
242
+ String regionId, String replicaSetName, String instanceType, String releaseNameOrNullForLatestMaster, Database databaseConfiguration,
243
+ String optionalKeyName, byte[] privateKeyEncryptionPassphrase, String replicaReplicationBearerToken, String optionalDomainName,
244
+ Integer optionalMemoryInMegabytesOrNull, String securityServiceReplicationBearerToken,
245
+ Integer optionalMemoryTotalSizeFactorOrNull, Integer optionalIgtimiRiotPort, URL continuationBaseURL) throws Exception {
246
+ assert getSecurityService().getCurrentUser() != null;
247
+ final AwsLandscape<String> landscape = getLandscape();
248
+ final String candidateHostname = getHostname(SharedLandscapeConstants.ARCHIVE_CANDIDATE_SUBDOMAIN, optionalDomainName);
249
+ final Iterable<ResourceRecordSet> existingDNSRulesForHostname = landscape.getResourceRecordSets(candidateHostname);
250
+ // Failing early in case DNS record already exists (see also bug 5826):
251
+ if (existingDNSRulesForHostname != null && !Util.isEmpty(existingDNSRulesForHostname)) {
252
+ throw new IllegalArgumentException("DNS record for "+candidateHostname+" already exists");
253
+ }
254
+ final AwsRegion region = new AwsRegion(regionId, landscape);
255
+ final Release release = getRelease(releaseNameOrNullForLatestMaster);
256
+ final AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> oldArchiveReplicaSet = getApplicationReplicaSet(
257
+ region, SharedLandscapeConstants.ARCHIVE_SERVER_APPLICATION_REPLICA_SET_NAME,
258
+ Landscape.WAIT_FOR_PROCESS_TIMEOUT.map(Duration::asMillis).orElse(null),
259
+ optionalKeyName, privateKeyEncryptionPassphrase);
260
+ final Integer oldArchiveMemoryInMB = getMemoryInMegabytes(optionalKeyName, privateKeyEncryptionPassphrase, oldArchiveReplicaSet.getMaster());
261
+ final com.sap.sailing.landscape.procedures.SailingAnalyticsMasterConfiguration.Builder<?, String> masterConfigurationBuilder =
262
+ createArchiveConfigurationBuilder(replicaSetName, databaseConfiguration, securityServiceReplicationBearerToken,
263
+ // if no memory size is specified, use that of existing production ARCHIVE server
264
+ optionalMemoryInMegabytesOrNull == null && optionalMemoryTotalSizeFactorOrNull == null ? oldArchiveMemoryInMB : optionalMemoryInMegabytesOrNull,
265
+ optionalMemoryTotalSizeFactorOrNull, optionalIgtimiRiotPort, region, release);
266
+ final String bearerTokenUsedByReplicas = getEffectiveBearerToken(replicaReplicationBearerToken);
267
+ final InboundReplicationConfiguration inboundMasterReplicationConfiguration = masterConfigurationBuilder.getInboundReplicationConfiguration().get();
268
+ establishServerAndServerGroupAndTryToMakeCurrentUserItsOwnerAndMember(replicaSetName, bearerTokenUsedByReplicas,
269
+ inboundMasterReplicationConfiguration.getMasterHostname(), inboundMasterReplicationConfiguration.getMasterHttpPort());
270
+ final ReverseProxyCluster<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>, RotatingFileBasedLog> reverseProxyCluster =
271
+ getLandscape().getReverseProxyCluster(region);
272
+ final com.sap.sailing.landscape.procedures.StartSailingAnalyticsMasterHost.Builder<?, String> masterHostBuilder = StartSailingAnalyticsMasterHost.masterHostBuilder(masterConfigurationBuilder);
273
+ masterHostBuilder
274
+ .setAvailabilityZone(getBestAvailabilityZoneForArchiveCandidate(region, landscape, oldArchiveReplicaSet.getMaster(), reverseProxyCluster, optionalKeyName, privateKeyEncryptionPassphrase))
275
+ .setInstanceName(SharedLandscapeConstants.ARCHIVE_SERVER_NEW_CANDIDATE_INSTANCE_NAME)
276
+ .setInstanceType(InstanceType.valueOf(instanceType))
277
+ .setOptionalTimeout(Landscape.WAIT_FOR_HOST_TIMEOUT)
278
+ .setLandscape(landscape)
279
+ .setRegion(region)
280
+ .setPrivateKeyEncryptionPassphrase(privateKeyEncryptionPassphrase);
281
+ if (optionalKeyName != null) {
282
+ masterHostBuilder.setKeyName(optionalKeyName);
283
+ }
284
+ final StartSailingAnalyticsMasterHost<String> masterHostStartProcedure = masterHostBuilder.build();
285
+ masterHostStartProcedure.run();
286
+ final SailingAnalyticsProcess<String> master = masterHostStartProcedure.getSailingAnalyticsProcess();
287
+ master.getHost().setTerminationProtection(true);
288
+ master.waitUntilAlive(Optional.of(Landscape.WAIT_FOR_HOST_TIMEOUT.get().plus(Landscape.WAIT_FOR_PROCESS_TIMEOUT.get())));
289
+ final AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet = landscape
290
+ .getApplicationReplicaSet(region, replicaSetName, master, /* replicas */ Collections.emptySet(),
291
+ Optional.of(Landscape.WAIT_FOR_HOST_TIMEOUT.get().plus(Landscape.WAIT_FOR_PROCESS_TIMEOUT.get())),
292
+ Optional.ofNullable(optionalKeyName), privateKeyEncryptionPassphrase);
293
+ final String privateIpAdress = master.getHost().getPrivateAddress().getHostAddress();
294
+ logger.info("Adding reverse proxy rule for archive candidate with hostname "+ candidateHostname + " and private ip address " + privateIpAdress);
295
+ reverseProxyCluster.setPlainRedirect(candidateHostname, master, Optional.ofNullable(optionalKeyName), privateKeyEncryptionPassphrase);
296
+ sendMailAboutNewArchiveCandidate(replicaSet);
297
+ final ScheduledExecutorService monitorTaskExecutor = ThreadPoolUtil.INSTANCE.getDefaultBackgroundTaskThreadPoolExecutor();
298
+ final ArchiveCandidateMonitoringBackgroundTask monitoringTask = new ArchiveCandidateMonitoringBackgroundTask(
299
+ getSecurityService().getCurrentUser(), this, replicaSet, candidateHostname, monitorTaskExecutor,
300
+ bearerTokenUsedByReplicas, continuationBaseURL);
301
+ monitorTaskExecutor.execute(monitoringTask);
302
+ }
303
+
304
+ private AwsAvailabilityZone getBestAvailabilityZoneForArchiveCandidate(AwsRegion region, AwsLandscape<String> landscape,
305
+ SailingAnalyticsProcess<String> oldArchivePrimary, ReverseProxyCluster<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>, RotatingFileBasedLog> reverseProxyCluster,
306
+ String optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception {
307
+ final AwsAvailabilityZone oldArchiveAZ = oldArchivePrimary.getHost().getAvailabilityZone();
308
+ AwsAvailabilityZone result = null;
309
+ for (final AwsInstance<String> reverseProxyHost : reverseProxyCluster.getHosts()) {
310
+ final AwsAvailabilityZone az = reverseProxyHost.getAvailabilityZone();
311
+ if (!az.equals(oldArchiveAZ)) {
312
+ result = az;
313
+ logger.info("Identified availability zone (AZ) " + result
314
+ + " for new ARCHIVE server; it differs from current ARCHIVE's AZ " + oldArchiveAZ
315
+ + " and has reverse proxy host " + reverseProxyHost.getInstanceId() + " in that AZ.");
316
+ break;
317
+ }
318
+ }
319
+ if (result == null) {
320
+ logger.warning(
321
+ "Couldn't find a reverse proxy in an availabililty zone different from that of the current ARCHIVE server ("
322
+ + oldArchiveAZ + "). Reverse proxies in AZs " + Util.joinStrings(", ",
323
+ Util.map(reverseProxyCluster.getHosts(), host -> host.getAvailabilityZone())));
324
+ // no AZ found that is not the same as for the current ARCHIVE server and also has a reverse proxy;
325
+ // now we have to choose between "a rock and a hard place:" either launch in an AZ where we don't have
326
+ // a reverse proxy and hence will see slightly less throughput and some additional cost for cross-AZ
327
+ // traffic; or launch in the same AZ the current ARCHIVE runs in; this will put the new failover (the
328
+ // current production ARCHIVE) and the new production ARCHIVE into the same AZ, not benefiting from
329
+ // the availability improvements incurred by running in multiple AZs.
330
+ result = Util.first(reverseProxyCluster.getHosts()).getAvailabilityZone();
331
+ logger.info("Choosing the AZ of the first reverse proxy to avoid cost and performance reduction by cross-AZ traffic: "+result);
332
+ }
333
+ return result;
334
+ }
335
+
336
+ @Override
337
+ public void makeCandidateArchiveServerGoLive(String regionId, String optionalKeyNameOrNull,
338
+ byte[] privateKeyEncryptionPassphrase, String optionalDomainName)
339
+ throws Exception {
340
+ final AwsLandscape<String> landscape = getLandscape();
341
+ final AwsRegion region = new AwsRegion(regionId, landscape);
342
+ final String candidateHostname = getHostname(SharedLandscapeConstants.ARCHIVE_CANDIDATE_SUBDOMAIN, optionalDomainName);
343
+ final AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> archiveReplicaSet = getApplicationReplicaSet(
344
+ region, SharedLandscapeConstants.ARCHIVE_SERVER_APPLICATION_REPLICA_SET_NAME,
345
+ Landscape.WAIT_FOR_PROCESS_TIMEOUT.map(Duration::asMillis).orElse(null),
346
+ optionalKeyNameOrNull, privateKeyEncryptionPassphrase);
347
+ if (archiveReplicaSet == null) {
348
+ throw new IllegalArgumentException("Couldn't find candidate replica set with name "
349
+ + SharedLandscapeConstants.ARCHIVE_SERVER_APPLICATION_REPLICA_SET_NAME + " in region " + regionId);
350
+ }
351
+ final SailingAnalyticsProcess<String> candidate = archiveReplicaSet.getMaster();
352
+ final ReverseProxy<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>, RotatingFileBasedLog> reverseProxyCluster =
353
+ getLandscape().getCentralReverseProxy(region);
354
+ final Optional<String> optionalKeyName = Optional.ofNullable(optionalKeyNameOrNull);
355
+ final Pair<String, String> archiveAndFailoverIPs = reverseProxyCluster
356
+ .getArchiveAndFailoverIPs(optionalKeyName, privateKeyEncryptionPassphrase);
357
+ logger.info("Found new candidate " + candidate.getHost()
358
+ .getInstanceId() + " with internal IP "
359
+ + candidate.getHost().getPrivateAddress() + " and current production ARCHIVE "
360
+ + archiveAndFailoverIPs.getA() + ". Turning production into failover and candidate into production.");
361
+ reverseProxyCluster.setArchiveAndFailoverIPs(candidate.getHost().getPrivateAddress().getHostAddress(),
362
+ archiveAndFailoverIPs.getA(), optionalKeyName, privateKeyEncryptionPassphrase);
363
+ try {
364
+ final SailingAnalyticsHost<String> oldProductionArchive = getLandscape().getHostByPrivateDnsNameOrIpAddress(region, archiveAndFailoverIPs.getA(), new SailingAnalyticsHostSupplier<>());
365
+ getLandscape().setInstanceName(oldProductionArchive, SharedLandscapeConstants.ARCHIVE_SERVER_FAILOVER_INSTANCE_NAME);
366
+ } catch (Exception e) {
367
+ logger.warning("Couldn't find old production ARCHIVE with IP "+archiveAndFailoverIPs.getA()+", so couldn't update its Name tag");
368
+ }
369
+ getLandscape().setInstanceName(candidate.getHost(), SharedLandscapeConstants.ARCHIVE_SERVER_INSTANCE_NAME);
370
+ logger.info("Removing reverse proxy rule for archive candidate with hostname "+ candidateHostname);
371
+ reverseProxyCluster.removeRedirect(candidateHostname, optionalKeyName, privateKeyEncryptionPassphrase);
372
+ try {
373
+ final SailingAnalyticsHost<String> oldFailover = getLandscape().getHostByPrivateDnsNameOrIpAddress(region,
374
+ archiveAndFailoverIPs.getB(), new SailingAnalyticsHostSupplier<>());
375
+ oldFailover.setTerminationProtection(false);
376
+ logger.info("Terminating old failover process, and hence probably host " + oldFailover.getInstanceId()
377
+ + " with internal IP " + oldFailover.getPrivateAddress());
378
+ for (final SailingAnalyticsProcess<String> applicationProcessOnOldFailover : oldFailover
379
+ .getApplicationProcesses(Landscape.WAIT_FOR_PROCESS_TIMEOUT, optionalKeyName,
380
+ privateKeyEncryptionPassphrase)) {
381
+ if (applicationProcessOnOldFailover
382
+ .getServerName(Landscape.WAIT_FOR_PROCESS_TIMEOUT, optionalKeyName,
383
+ privateKeyEncryptionPassphrase)
384
+ .equals(SharedLandscapeConstants.ARCHIVE_SERVER_APPLICATION_REPLICA_SET_NAME)) {
385
+ applicationProcessOnOldFailover.stopAndTerminateIfLast(Landscape.WAIT_FOR_PROCESS_TIMEOUT, optionalKeyName, privateKeyEncryptionPassphrase);
386
+ }
387
+ }
388
+ } catch (Exception e) {
389
+ logger.warning("Issue trying to clean up old failover instance: "+e.getMessage());
390
+ }
391
+ sendMailAboutNewArchiveServerLive(archiveReplicaSet);
392
+ }
393
+
394
+ @Override
239 395
public AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> deployApplicationToExistingHost(String replicaSetName,
240 396
SailingAnalyticsHost<String> hostToDeployTo, String replicaInstanceType, boolean dynamicLoadBalancerMapping,
241 397
String releaseNameOrNullForLatestMaster, String optionalKeyName, byte[] privateKeyEncryptionPassphrase,
... ...
@@ -352,7 +508,7 @@ public class LandscapeServiceImpl implements LandscapeService {
352 508
optionalIgtimiRiotPort, region, release);
353 509
final InboundReplicationConfiguration inboundMasterReplicationConfiguration = masterConfigurationBuilder.getInboundReplicationConfiguration().get();
354 510
final String bearerTokenUsedByReplicas = getEffectiveBearerToken(replicaReplicationBearerToken);
355
- establishServerGroupAndTryToMakeCurrentUserItsOwnerAndMember(replicaSetName, bearerTokenUsedByReplicas,
511
+ establishServerAndServerGroupAndTryToMakeCurrentUserItsOwnerAndMember(replicaSetName, bearerTokenUsedByReplicas,
356 512
inboundMasterReplicationConfiguration.getMasterHostname(), inboundMasterReplicationConfiguration.getMasterHttpPort());
357 513
final SailingAnalyticsProcess<String> master = deployProcessToSharedInstance(hostToDeployTo,
358 514
masterConfigurationBuilder, optionalKeyName, privateKeyEncryptionPassphrase);
... ...
@@ -805,7 +961,7 @@ public class LandscapeServiceImpl implements LandscapeService {
805 961
: SailingReleaseRepository.INSTANCE.getRelease(releaseNameOrNullForLatestMaster);
806 962
}
807 963
808
- private void establishServerGroupAndTryToMakeCurrentUserItsOwnerAndMember(String serverName,
964
+ private void establishServerAndServerGroupAndTryToMakeCurrentUserItsOwnerAndMember(String serverName,
809 965
String bearerTokenUsedByReplicas, String securityServiceHostname,
810 966
Integer securityServicePort)
811 967
throws MalformedURLException, ClientProtocolException, IOException, ParseException, IllegalAccessException {
... ...
@@ -815,6 +971,7 @@ public class LandscapeServiceImpl implements LandscapeService {
815 971
RemoteServerUtil.getBaseServerUrl(securityServiceHostname, securityServicePort==null?443:securityServicePort), bearerTokenUsedByReplicas);
816 972
final UUID userGroupId = securityServiceServer.getUserGroupIdByName(serverGroupName);
817 973
final UUID groupId;
974
+ final String securityServiceServerUsername = securityServiceServer.getUsername();
818 975
if (userGroupId != null) {
819 976
groupId = userGroupId;
820 977
final TypeRelativeObjectIdentifier serverGroupTypeRelativeObjectId = new TypeRelativeObjectIdentifier(userGroupId.toString());
... ...
@@ -829,7 +986,7 @@ public class LandscapeServiceImpl implements LandscapeService {
829 986
SecuredSecurityTypes.SERVER.getPermissionForTypeRelativeIdentifier(DefaultActions.DELETE, serverGroupTypeRelativeObjectId)));
830 987
for (final Pair<WildcardPermission, Boolean> permission : permissions) {
831 988
if (!permission.getB()) {
832
- final String msg = "Subject "+securityServiceServer.getUsername()+" on server "+securityServiceHostname+
989
+ final String msg = "Subject "+securityServiceServerUsername+" on server "+securityServiceHostname+
833 990
" is not allowed "+permission.getA()+". Not allowing to create application replica set for "+serverName;
834 991
logger.warning(msg);
835 992
throw new AuthorizationException(msg);
... ...
@@ -841,12 +998,12 @@ public class LandscapeServiceImpl implements LandscapeService {
841 998
} else {
842 999
groupId = securityServiceServer.createUserGroupAndAddCurrentUser(serverGroupName);
843 1000
try {
844
- securityServiceServer.addRoleToUser(ServerAdminRole.getInstance().getId(), securityServiceServer.getUsername(),
1001
+ securityServiceServer.addRoleToUser(ServerAdminRole.getInstance().getId(), securityServiceServerUsername,
845 1002
/* qualified for server group: */ groupId, null, /* transitive */ true);
846 1003
} catch (Exception e) {
847 1004
// this didn't work, but it's not the end of the world if we cannot grant the requesting user the
848 1005
// event_manager:{group-name} role; the user may end up not having SERVER:CREATE_OBJECT...
849
- logger.warning("Couldn't grant role "+ServerAdminRole.getInstance().getName()+" to user "+securityServiceServer.getUsername()+": "+e.getMessage());
1006
+ logger.warning("Couldn't grant role "+ServerAdminRole.getInstance().getName()+" to user "+securityServiceServerUsername+": "+e.getMessage());
850 1007
}
851 1008
try {
852 1009
// try to set the group owner of the new group to the group itself, allowing all users with role user:{group-name} to
... ...
@@ -861,6 +1018,14 @@ public class LandscapeServiceImpl implements LandscapeService {
861 1018
}
862 1019
}
863 1020
ensureGroupMembersCanReadGroup(securityServiceServer, groupId);
1021
+ final TypeRelativeObjectIdentifier serverTypeRelativeObjectId = new TypeRelativeObjectIdentifier(serverName);
1022
+ final Pair<UUID, String> serverOwningGroupIdAndUsername = securityServiceServer.getGroupAndUserOwner(SecuredSecurityTypes.SERVER, serverTypeRelativeObjectId);
1023
+ if (serverOwningGroupIdAndUsername == null || serverOwningGroupIdAndUsername.getA() == null && serverOwningGroupIdAndUsername.getB() == null) {
1024
+ logger.info("Setting ownership for SERVER object "+serverName+" to group "+serverGroupName+" and user "+securityServiceServerUsername);
1025
+ securityServiceServer.setGroupAndUserOwner(SecuredSecurityTypes.SERVER, serverTypeRelativeObjectId,
1026
+ Optional.of(SecuredSecurityTypes.SERVER.getName()+"/"+serverName),
1027
+ Optional.of(groupId), Optional.of(securityServiceServerUsername));
1028
+ }
864 1029
}
865 1030
866 1031
private void ensureGroupMembersCanReadGroup(SailingServer securityServiceServer, UUID groupId) throws ClientProtocolException, IOException, ParseException {
... ...
@@ -895,6 +1060,29 @@ public class LandscapeServiceImpl implements LandscapeService {
895 1060
applyMemoryConfigurationToApplicationConfigurationBuilder(masterConfigurationBuilder, optionalMemoryInMegabytesOrNull, optionalMemoryTotalSizeFactorOrNull);
896 1061
return masterConfigurationBuilder;
897 1062
}
1063
+
1064
+ private <AppConfigBuilderT extends com.sap.sailing.landscape.procedures.SailingAnalyticsMasterConfiguration.Builder<AppConfigBuilderT, String>> AppConfigBuilderT createArchiveConfigurationBuilder(
1065
+ String replicaSetName, Database databaseConfiguration, String optionalMasterReplicationBearerTokenOrNull, Integer optionalMemoryInMegabytesOrNull,
1066
+ Integer optionalMemoryTotalSizeFactorOrNull, Integer optionalIgtimiRiotPort, final AwsRegion region, final Release release) {
1067
+ final AppConfigBuilderT masterConfigurationBuilder = SailingAnalyticsMasterConfiguration.masterBuilder();
1068
+ final String bearerTokenUsedByMaster = getEffectiveBearerToken(optionalMasterReplicationBearerTokenOrNull);
1069
+ final User currentUser = getSecurityService().getCurrentUser();
1070
+ if (currentUser != null && currentUser.isEmailValidated() && currentUser.getEmail() != null) {
1071
+ masterConfigurationBuilder.setCommaSeparatedEmailAddressesToNotifyOfStartup(currentUser.getEmail());
1072
+ }
1073
+ masterConfigurationBuilder
1074
+ .setDatabaseConfiguration(databaseConfiguration)
1075
+ .setLandscape(getLandscape())
1076
+ .setServerName(replicaSetName)
1077
+ .setRelease(release)
1078
+ .setRegion(region)
1079
+ .setInboundReplicationConfiguration(InboundReplicationConfiguration.builder().setCredentials(new BearerTokenReplicationCredentials(bearerTokenUsedByMaster)).build());
1080
+ if (optionalIgtimiRiotPort != null) {
1081
+ masterConfigurationBuilder.setIgtimiRiotPort(optionalIgtimiRiotPort);
1082
+ }
1083
+ applyMemoryConfigurationToApplicationConfigurationBuilder(masterConfigurationBuilder, optionalMemoryInMegabytesOrNull, optionalMemoryTotalSizeFactorOrNull);
1084
+ return masterConfigurationBuilder;
1085
+ }
898 1086
899 1087
/**
900 1088
* No specific memory configuration is made here; replicas are mostly launched on a dedicated host and hence can
... ...
@@ -1648,6 +1836,32 @@ public class LandscapeServiceImpl implements LandscapeService {
1648 1836
return getLandscape().getApplicationReplicaSet(region, replicaSet.getServerName(), newMaster, replicaSet.getReplicas(),
1649 1837
Landscape.WAIT_FOR_PROCESS_TIMEOUT, Optional.ofNullable(optionalKeyName), privateKeyEncryptionPassphrase);
1650 1838
}
1839
+
1840
+ private void sendMailAboutNewArchiveCandidate(
1841
+ AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet) throws MailException {
1842
+ sendMailToCurrentUser("StartingNewArchiveCandidateSubject", "StartingNewArchiveCandidateBody", replicaSet.getServerName());
1843
+ sendMailToReplicaSetOwner(replicaSet, "RefrainFromArchivingSubject", "RefrainFromArchivingBody", Optional.empty());
1844
+ }
1845
+
1846
+ @Override
1847
+ public void sendMailToCurrentUser(String messageSubjectKey, String messageBodyKey, String... messageParameters) throws MailException {
1848
+ final User currentUser = getSecurityService().getCurrentUser();
1849
+ sendMailToUser(currentUser, messageSubjectKey, messageBodyKey, messageParameters);
1850
+ }
1851
+
1852
+ @Override
1853
+ public void sendMailToUser(final User user, String messageSubjectKey, String messageBodyKey,
1854
+ String... messageParameters) throws MailException {
1855
+ final ResourceBundleStringMessages stringMessages = ResourceBundleStringMessages.create(STRING_MESSAGES_BASE_NAME, getClass().getClassLoader(), StandardCharsets.UTF_8.name());
1856
+ if (user != null && user.isEmailValidated()) {
1857
+ final String subject = stringMessages.get(user.getLocaleOrDefault(), messageSubjectKey, messageParameters);
1858
+ final String body = stringMessages.get(user.getLocaleOrDefault(), messageBodyKey, messageParameters);
1859
+ getSecurityService().sendMail(user.getName(), subject, body);
1860
+ } else {
1861
+ logger.warning("Not sending e-mail about new archive candidate to current user because no user is logged in or email address of logged in user "+
1862
+ (user == null ? "" : user.getName()+" ")+"is not validated");
1863
+ }
1864
+ }
1651 1865
1652 1866
private void sendMailAboutMasterUnavailable(
1653 1867
AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet) throws MailException {
... ...
@@ -1669,7 +1883,8 @@ public class LandscapeServiceImpl implements LandscapeService {
1669 1883
* action on the {@code replicaSet} will receive the e-mail in addition to the server owner. No user
1670 1884
* will receive the e-mail twice.
1671 1885
*/
1672
- private void sendMailToReplicaSetOwner(
1886
+ @Override
1887
+ public void sendMailToReplicaSetOwner(
1673 1888
AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet,
1674 1889
final String subjectMessageKey, final String bodyMessageKey, Optional<Action> alsoSendToAllUsersWithThisPermissionOnReplicaSet) throws MailException {
1675 1890
final Iterable<User> usersToSendMailTo = getSecurityService().getUsersToInformAboutReplicaSet(replicaSet.getServerName(), alsoSendToAllUsersWithThisPermissionOnReplicaSet);
... ...
@@ -1687,6 +1902,15 @@ public class LandscapeServiceImpl implements LandscapeService {
1687 1902
}
1688 1903
}
1689 1904
}
1905
+
1906
+ /**
1907
+ * This is to be called after the rotation of candidate/production/failover ARCHIVE has happend, to inform
1908
+ */
1909
+ private void sendMailAboutNewArchiveServerLive(
1910
+ AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet) throws MailException {
1911
+ sendMailToReplicaSetOwner(replicaSet, "NewArchiveServerLiveSubject", "NewArchiveServerLiveBody",
1912
+ Optional.of(ServerActions.CONFIGURE_LOCAL_SERVER));
1913
+ }
1690 1914
1691 1915
/**
1692 1916
* If a non-{@code null}, non-{@link String#isEmpty() empty} bearer token is provided by the
... ...
@@ -2031,4 +2255,9 @@ public class LandscapeServiceImpl implements LandscapeService {
2031 2255
final Integer memoryInMegabytes = JvmUtils.getMegabytesFromJvmSize(memoryInMegabytesAsString).orElse(null);
2032 2256
return memoryInMegabytes;
2033 2257
}
2258
+
2259
+ @Override
2260
+ public SailingServerFactory getSailingServerFactory() {
2261
+ return sailingServerFactoryTracker.getService();
2262
+ }
2034 2263
}
java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/impl/SailingAnalyticsProcessImpl.java
... ...
@@ -51,6 +51,11 @@ implements SailingAnalyticsProcess<ShardingKey> {
51 51
private static final String STATUS_SERVERDIRECTORY_PROPERTY_NAME = "serverdirectory";
52 52
private static final String STATUS_RELEASE_PROPERTY_NAME = "release";
53 53
private static final String MONGODB_CONFIGURATION_PROPERTY_NAME = "mongoDbConfiguration";
54
+ private static final String SYSTEM_LOAD_AVERAGE_LAST_MINUTE_NAME = "systemloadaveragelastminute";
55
+ private static final String DEFAULT_BACKGROUND_THREAD_POOL_EXECUTOR_QUEUE_LENGTH_NAME = "defaultbackgroundthreadpoolexecutorqueuelength";
56
+ private static final String DEFAULT_FOREGROUND_THREAD_POOL_EXECUTOR_QUEUE_LENGTH_NAME = "defaultforegroundthreadpoolexecutorqueuelength";
57
+ private static final String DEFAULT_BACKGROUND_THREAD_POOL_EXECUTOR_QUEUE_LENGTH_NONDELAYED_NAME = "defaultbackgroundthreadpoolexecutorqueuelengthnondelayed";
58
+ private static final String DEFAULT_FOREGROUND_THREAD_POOL_EXECUTOR_QUEUE_LENGTH_NONDELAYED_NAME = "defaultforegroundthreadpoolexecutorqueuelengthnondelayed";
54 59
private Integer expeditionUdpPort;
55 60
private Integer igtimiRiotPort;
56 61
private Release release;
... ...
@@ -127,6 +132,36 @@ implements SailingAnalyticsProcess<ShardingKey> {
127 132
}
128 133
129 134
@Override
135
+ public double getLastMinuteSystemLoadAverage(Optional<Duration> optionalTimeout) throws TimeoutException, Exception {
136
+ final JSONObject status = getStatus(optionalTimeout);
137
+ return ((Number) status.get(SYSTEM_LOAD_AVERAGE_LAST_MINUTE_NAME)).doubleValue();
138
+ }
139
+
140
+ @Override
141
+ public int getDefaultBackgroundThreadPoolExecutorQueueSize(Optional<Duration> optionalTimeout) throws TimeoutException, Exception {
142
+ final JSONObject status = getStatus(optionalTimeout);
143
+ return ((Number) status.get(DEFAULT_BACKGROUND_THREAD_POOL_EXECUTOR_QUEUE_LENGTH_NAME)).intValue();
144
+ }
145
+
146
+ @Override
147
+ public int getDefaultForegroundThreadPoolExecutorQueueSize(Optional<Duration> optionalTimeout) throws TimeoutException, Exception {
148
+ final JSONObject status = getStatus(optionalTimeout);
149
+ return ((Number) status.get(DEFAULT_FOREGROUND_THREAD_POOL_EXECUTOR_QUEUE_LENGTH_NAME)).intValue();
150
+ }
151
+
152
+ @Override
153
+ public int getDefaultBackgroundThreadPoolExecutorQueueSizeNondelayed(Optional<Duration> optionalTimeout) throws TimeoutException, Exception {
154
+ final JSONObject status = getStatus(optionalTimeout);
155
+ return ((Number) status.get(DEFAULT_BACKGROUND_THREAD_POOL_EXECUTOR_QUEUE_LENGTH_NONDELAYED_NAME)).intValue();
156
+ }
157
+
158
+ @Override
159
+ public int getDefaultForegroundThreadPoolExecutorQueueSizeNondelayed(Optional<Duration> optionalTimeout) throws TimeoutException, Exception {
160
+ final JSONObject status = getStatus(optionalTimeout);
161
+ return ((Number) status.get(DEFAULT_FOREGROUND_THREAD_POOL_EXECUTOR_QUEUE_LENGTH_NONDELAYED_NAME)).intValue();
162
+ }
163
+
164
+ @Override
130 165
public Database getDatabaseConfiguration(Region region, Optional<Duration> optionalTimeout,
131 166
Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception {
132 167
final JSONObject mongoDBConfiguration = (JSONObject) getStatus(optionalTimeout).get(MONGODB_CONFIGURATION_PROPERTY_NAME);
java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/CompareServersResource.java
... ...
@@ -19,9 +19,11 @@ import java.util.UUID;
19 19
import java.util.logging.Logger;
20 20
21 21
import javax.ws.rs.FormParam;
22
+import javax.ws.rs.GET;
22 23
import javax.ws.rs.POST;
23 24
import javax.ws.rs.Path;
24 25
import javax.ws.rs.Produces;
26
+import javax.ws.rs.QueryParam;
25 27
import javax.ws.rs.core.Context;
26 28
import javax.ws.rs.core.Response;
27 29
import javax.ws.rs.core.Response.Status;
... ...
@@ -49,8 +51,8 @@ public class CompareServersResource extends AbstractSailingServerResource {
49 51
50 52
public static final Logger logger = Logger.getLogger(CompareServersResource.class.getName());
51 53
52
- private static final String LEADERBOARDGROUPSPATH = "/sailingserver/api/v1/leaderboardgroups";
53
- private static final String LEADERBOARDGROUPSIDENTIFIABLEPATH = LEADERBOARDGROUPSPATH+"/identifiable";
54
+ private static final String LEADERBOARDGROUPSPATH = "/sailingserver/api"+LeaderboardGroupsResource.V1_LEADERBOARDGROUPS;
55
+ private static final String LEADERBOARDGROUPSIDENTIFIABLEPATH = LEADERBOARDGROUPSPATH+LeaderboardGroupsResource.IDENTIFIABLE;
54 56
public static final String SERVER1_FORM_PARAM = "server1";
55 57
public static final String SERVER2_FORM_PARAM = "server2";
56 58
public static final String USER1_FORM_PARAM = "user1";
... ...
@@ -106,6 +108,25 @@ public class CompareServersResource extends AbstractSailingServerResource {
106 108
UriInfo uriInfo;
107 109
108 110
/**
111
+ * Forwards to the POST method; user authentication is taken from the request's authentication. Therefore, no
112
+ * separate authentications for the two servers to compare can be provided. The server receiving this request will
113
+ * act as the default for "server1". This is a convenience method for quick comparisons of two
114
+ * servers without needing to provide authentication information, assuming that the server receiving this request
115
+ * shares its security service with the server specified as "server2" through replication, and that the user
116
+ * authenticated for this request has access to both servers. For more complex use cases, use the POST method
117
+ * directly.
118
+ */
119
+ @GET
120
+ @Produces("application/json;charset=UTF-8")
121
+ public Response compareServersGet(
122
+ @QueryParam(SERVER1_FORM_PARAM) String server1,
123
+ @QueryParam(SERVER2_FORM_PARAM) String server2,
124
+ @QueryParam(LEADERBOARDGROUP_UUID_FORM_PARAM) Set<String> uuidset) throws MalformedURLException {
125
+ return compareServers(server1, server2, uuidset, /* user1 */ null, /* user2 */ null, /* password1 */ null,
126
+ /* password2 */ null, /* bearer1 */ null, /* bearer2 */ null);
127
+ }
128
+
129
+ /**
109 130
* @param server1
110 131
* optional; if not provided, the server receiving this request will act as the default for "server1"
111 132
* @param user1
... ...
@@ -204,7 +225,7 @@ public class CompareServersResource extends AbstractSailingServerResource {
204 225
}
205 226
}
206 227
}
207
- JSONObject json = new JSONObject();
228
+ final JSONObject json = new JSONObject();
208 229
for (Entry<String, Set<Object>> entry : result.entrySet()) {
209 230
json.put(entry.getKey(), entry.getValue());
210 231
}
java/com.sap.sailing.server.gateway/webservices/api/v1/compareServers.html
... ...
@@ -33,7 +33,7 @@
33 33
<td>json</td>
34 34
</tr>
35 35
<tr>
36
- <td>Mandatory query parameters:</td>
36
+ <td>Mandatory form parameters:</td>
37 37
<td>
38 38
<div>server2, the hostname for the second server.</div>
39 39
</td>
... ...
@@ -146,6 +146,48 @@
146 146
</pre>
147 147
</td>
148 148
</tr>
149
+ <tr>
150
+ <td>Webservice Type:</td>
151
+ <td>GET</td>
152
+ </tr>
153
+ <tr>
154
+ <td>Output format:</td>
155
+ <td>json</td>
156
+ </tr>
157
+ <tr>
158
+ <td>Mandatory query parameters:</td>
159
+ <td>
160
+ <div>server2, the hostname for the second server.</div>
161
+ </td>
162
+ </tr>
163
+ <tr>
164
+ <td>Optional query parameters:</td>
165
+ <td>
166
+ <div><tt>server1</tt>, the hostname for the first server. Defaults to the target of your request</div>
167
+ <div><tt>leaderboardgroupUUID[]</tt>, the leaderboardgroup UUID you want to compare.
168
+ Use multiple times if you need to compare more than one leaderboardgroup. Don't specify if you'd like
169
+ to compare <em>all</em> leaderboard groups of both servers with each other.</div>
170
+ </td>
171
+ The GET method can easily be used from a Browser UI. It uses any session/cookie credentials or may be used
172
+ with basic authentication. No authentication information can be provided as query parameters with the GET method.
173
+ </tr>
174
+ <tr>
175
+ <td>Request method:</td>
176
+ <td>GET</td>
177
+ </tr>
178
+ <tr>
179
+ <td>Example:</td>
180
+ <td>curl http://127.0.0.1:8888/sailingserver/api/v1/compareservers?server2=127.0.0.1:8889&leaderboardgroupUUID[]=0334e85e-ebc9-4835-b44d-5db71245b9e9&leaderboardgroupUUID[]=78476ac8-5519-4c3f-9c6d-670468edf4fc" -H "Authorization: Basic YWRtaW46YWRtaW4="<br>
181
+ produces output:<pre>
182
+ {
183
+ 127.0.0.1:8889 []
184
+ 127.0.0.1:8888 []
185
+ }
186
+ </pre>
187
+ </td>
188
+ </tr>
189
+ </td>
190
+ </tr>
149 191
</table>
150 192
<div style="height: 1em;"></div>
151 193
<a href="index.html">Back to Web Service Overview</a>
java/com.sap.sailing.www/release_notes_admin.html
... ...
@@ -23,6 +23,18 @@
23 23
<div class="mainContent">
24 24
<h2 class="releaseHeadline">Release Notes - Administration Console</h2>
25 25
<div class="innerContent">
26
+ <h2 class="articleSubheadline">February 2026</h2>
27
+ <ul class="bulletList">
28
+ <li>Implemented an automated procedure for upgrading the ARCHIVE server. This is now available
29
+ through the "Upgrade" action icon in the landscape management panel for "phase 1" (launching,
30
+ setting https://archive-candidate.sapsailing.com as redirect, and scheduling periodic
31
+ checks in the background for completion of loading and calculations as well as a final
32
+ "compare servers"), and the "Activate ARCHIVE Candidate" action icon for "phase 2" which
33
+ will then, after the user has confirmed having done some manual spot checks, rotate the
34
+ new candidate to the live server, and the former live server to the "fail-over" role,
35
+ renaming instances, terminating the old "fail-over" instance and updating all
36
+ reverse proxies in the landscape accordingly. User information is sent via e-mail.</li>
37
+ </ul>
26 38
<h2 class="articleSubheadline">January 2026</h2>
27 39
<ul class="bulletList">
28 40
<li>Extended RabbitMQ channel heartbeat interval to 1h. This will suffice even for
java/com.sap.sse.gwt/resources/com/sap/sse/gwt/client/images/check.png
... ...
Binary files /dev/null and b/java/com.sap.sse.gwt/resources/com/sap/sse/gwt/client/images/check.png differ
java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/IconResources.java
... ...
@@ -85,4 +85,7 @@ public interface IconResources extends ClientBundle {
85 85
86 86
@Source("images/command_symbol.png")
87 87
ImageResource commandSymbol();
88
+
89
+ @Source("images/check.png")
90
+ ImageResource check();
88 91
}
java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/dialog/DataEntryDialog.java
... ...
@@ -281,7 +281,7 @@ public abstract class DataEntryDialog<T> {
281 281
*
282 282
* @see #createSuggestBox(SuggestOracle)
283 283
*/
284
- protected SuggestBox createSuggestBox(Iterable<String> suggestValues) {
284
+ public SuggestBox createSuggestBox(Iterable<String> suggestValues) {
285 285
List<String> suggestValuesAsCollection = new ArrayList<>();
286 286
Util.addAll(suggestValues, suggestValuesAsCollection);
287 287
final MultiWordSuggestOracle oracle = new MultiWordSuggestOracle();
... ...
@@ -299,7 +299,7 @@ public abstract class DataEntryDialog<T> {
299 299
*
300 300
* @see SuggestBox#SuggestBox(SuggestOracle)
301 301
*/
302
- protected SuggestBox createSuggestBox(SuggestOracle suggestOracle) {
302
+ public SuggestBox createSuggestBox(SuggestOracle suggestOracle) {
303 303
final SuggestBox result = new SuggestBox(suggestOracle);
304 304
ensureHasValueIsValidated(result.getValueBox());
305 305
ensureChangeableIsValidated(result.getValueBox());
java/com.sap.sse.landscape.aws.test/src/com/sap/sse/landscape/aws/MongoUriParserTest.java
... ...
@@ -36,12 +36,12 @@ public class MongoUriParserTest {
36 36
when(host1.getPublicAddress()).thenReturn(wwwExampleCom);
37 37
when(host1.getPrivateAddress()).thenReturn(wwwExampleCom);
38 38
when(host1.getHostname()).thenReturn(WWW_EXAMPLE_COM);
39
- when(landscape.getHostByPrivateIpAddress(ArgumentMatchers.any(Region.class), ArgumentMatchers.contains(wwwExampleCom.getHostAddress()), ArgumentMatchers.any(HostSupplier.class))).thenReturn(host1);
39
+ when(landscape.getHostByPrivateDnsNameOrIpAddress(ArgumentMatchers.any(Region.class), ArgumentMatchers.contains(wwwExampleCom.getHostAddress()), ArgumentMatchers.any(HostSupplier.class))).thenReturn(host1);
40 40
final InetAddress loopback = InetAddress.getLoopbackAddress();
41 41
final AwsInstance<String> host2 = mock(AwsInstance.class);
42 42
when(host2.getPrivateAddress()).thenReturn(loopback);
43 43
when(host2.getHostname()).thenReturn(loopback.getHostAddress());
44
- when(landscape.getHostByPrivateIpAddress(ArgumentMatchers.any(Region.class), ArgumentMatchers.contains(loopback.getHostAddress()), ArgumentMatchers.any(HostSupplier.class))).thenReturn(host2);
44
+ when(landscape.getHostByPrivateDnsNameOrIpAddress(ArgumentMatchers.any(Region.class), ArgumentMatchers.contains(loopback.getHostAddress()), ArgumentMatchers.any(HostSupplier.class))).thenReturn(host2);
45 45
parser = new MongoUriParser<String>(landscape, new AwsRegion("eu-west-2", landscape));
46 46
}
47 47
java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/AwsInstance.java
... ...
@@ -53,6 +53,8 @@ public interface AwsInstance<ShardingKey> extends Host {
53 53
default String getId() {
54 54
return getInstanceId();
55 55
}
56
+
57
+ void setTerminationProtection(boolean terminationProtection);
56 58
57 59
void terminate();
58 60
java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/AwsLandscape.java
... ...
@@ -185,7 +185,7 @@ public interface AwsLandscape<ShardingKey> extends Landscape<ShardingKey> {
185 185
* private key; see also {@link #getKeyPairInfo(Region, String)}
186 186
* @param userData
187 187
* zero or more strings representing the user data to be passed to the instance; multiple strings will be
188
- * concatenated, using the line separator to join them. The instance is able to read the user data throuh
188
+ * concatenated, using the line separator to join them. The instance is able to read the user data through
189 189
* the AWS SDK installed on the instance.
190 190
*/
191 191
default <HostT extends AwsInstance<ShardingKey>> HostT launchHost(
... ...
@@ -262,7 +262,7 @@ public interface AwsLandscape<ShardingKey> extends Landscape<ShardingKey> {
262 262
*/
263 263
<HostT extends AwsInstance<ShardingKey>> Iterable<HostT> getRunningHostsWithTag(Region region, String tagName, HostSupplier<ShardingKey, HostT> hostSupplier);
264 264
265
- <HostT extends AwsInstance<ShardingKey>> HostT getHostByPrivateIpAddress(Region region, String publicIpAddress,
265
+ <HostT extends AwsInstance<ShardingKey>> HostT getHostByPrivateDnsNameOrIpAddress(Region region, String privateDnsNameOrIpAddress,
266 266
HostSupplier<ShardingKey, HostT> hostSupplier);
267 267
268 268
<HostT extends AwsInstance<ShardingKey>> HostT getHostByPublicIpAddress(Region region, String publicIpAddress,
... ...
@@ -288,6 +288,8 @@ public interface AwsLandscape<ShardingKey> extends Landscape<ShardingKey> {
288 288
*/
289 289
SSHKeyPair importKeyPair(Region region, byte[] publicKey, byte[] encryptedPrivateKey, String keyName) throws JSchException;
290 290
291
+ void setTerminationProtection(AwsInstance<ShardingKey> host, boolean terminationProtection);
292
+
291 293
void terminate(AwsInstance<ShardingKey> host);
292 294
293 295
/**
... ...
@@ -329,7 +331,7 @@ public interface AwsLandscape<ShardingKey> extends Landscape<ShardingKey> {
329 331
330 332
Instance getInstanceByPublicIpAddress(Region region, String publicIpAddress);
331 333
332
- Instance getInstanceByPrivateIpAddress(Region region, String publicIpAddress);
334
+ Instance getInstanceByPrivateDnsNameOrIpAddress(Region region, String privateDnsNameOrIpAddress);
333 335
334 336
/**
335 337
* @param hostname
... ...
@@ -890,4 +892,6 @@ public interface AwsLandscape<ShardingKey> extends Landscape<ShardingKey> {
890 892
* Removes hosts from an IP-based target group.
891 893
*/
892 894
void removeIpTargetFromTargetGroup(TargetGroup<ShardingKey> targetGroup, Iterable<AwsInstance<ShardingKey>> hosts);
895
+
896
+ void setInstanceName(AwsInstance<ShardingKey> host, String newInstanceName);
893 897
}
java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/MongoUriParser.java
... ...
@@ -1,6 +1,5 @@
1 1
package com.sap.sse.landscape.aws;
2 2
3
-import java.net.InetAddress;
4 3
import java.net.URI;
5 4
import java.net.URISyntaxException;
6 5
import java.net.UnknownHostException;
... ...
@@ -156,8 +155,7 @@ public class MongoUriParser<ShardingKey> {
156 155
* If the host isn't found in the landscape, the {@link Pair#getA()} component of the pair returned will be {@code null}.
157 156
*/
158 157
private Pair<AwsInstance<ShardingKey>, Integer> getHostAndPort(String hostname, Integer optionalPort) throws UnknownHostException {
159
- final InetAddress address = InetAddress.getByName(hostname);
160
- final AwsInstance<ShardingKey> hostByPrivateIp = landscape.getHostByPrivateIpAddress(region, address.getHostAddress(), AwsInstanceImpl::new);
161
- return new Pair<>(hostByPrivateIp, optionalPort);
158
+ final AwsInstance<ShardingKey> hostByPrivateDnsOrIp = landscape.getHostByPrivateDnsNameOrIpAddress(region, hostname, AwsInstanceImpl::new);
159
+ return new Pair<>(hostByPrivateDnsOrIp, optionalPort);
162 160
}
163 161
}
java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/ReverseProxy.java
... ...
@@ -2,6 +2,8 @@ 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.Util.Pair;
5 7
import com.sap.sse.landscape.Host;
6 8
import com.sap.sse.landscape.Log;
7 9
import com.sap.sse.landscape.application.ApplicationProcess;
... ...
@@ -124,4 +126,13 @@ public interface ReverseProxy<ShardingKey, MetricsT extends ApplicationProcessMe
124 126
* member instances and their availability zones.
125 127
*/
126 128
String getTargetGroupHealthCheckPath(String targetGroupArn);
129
+
130
+ /**
131
+ * Fetches the ARCHIVE_IP and ARCHIVE_FAILOVER_IP definitions from the 000-macros.conf file in the reverse proxy and returns
132
+ * them, in this order, as strings.
133
+ */
134
+ Pair<String, String> getArchiveAndFailoverIPs(Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception;
135
+
136
+ void setArchiveAndFailoverIPs(String productionArchiveServerInternalIPAddress, String failoverArchiveServerInternalIPAddress,
137
+ Optional<String> ofNullable, byte[] privateKeyEncryptionPassphrase) throws Exception;
127 138
}
java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/ApacheReverseProxy.java
... ...
@@ -6,7 +6,9 @@ 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
+
9 10
import com.sap.sse.common.Duration;
11
+import com.sap.sse.common.Util.Pair;
10 12
import com.sap.sse.landscape.Host;
11 13
import com.sap.sse.landscape.RotatingFileBasedLog;
12 14
import com.sap.sse.landscape.application.ApplicationProcess;
... ...
@@ -76,6 +78,20 @@ implements com.sap.sse.landscape.Process<RotatingFileBasedLog, MetricsT> {
76 78
private static final String HOME_ARCHIVE_REDIRECT_MACRO = "Home-ARCHIVE";
77 79
private static final String EVENT_ARCHIVE_REDIRECT_MACRO = "Event-ARCHIVE";
78 80
private static final String SERIES_ARCHIVE_REDIRECT_MACRO = "Series-ARCHIVE";
81
+ private static final String CONFIG_FILE_FOR_ARCHIVE_AND_FAILOVER_DEFINITION = "000-macros"+CONFIG_FILE_EXTENSION;
82
+
83
+ /**
84
+ * Name of the "macro"/variable definition used in the file identified by {@link #CONFIG_FILE_FOR_ARCHIVE_AND_FAILOVER_DEFINITION}
85
+ * that specifies the internal IP address of the primary ARCHIVE server to use.
86
+ */
87
+ private static final String ARCHIVE_IP = "ARCHIVE_IP";
88
+
89
+ /**
90
+ * Name of the "macro"/variable definition used in the file identified by {@link #CONFIG_FILE_FOR_ARCHIVE_AND_FAILOVER_DEFINITION}
91
+ * that specifies the internal IP address of the fail-over ARCHIVE server to use.
92
+ */
93
+ private static final String ARCHIVE_FAILOVER_IP = "ARCHIVE_FAILOVER_IP";
94
+
79 95
private final AwsInstance<ShardingKey> host;
80 96
81 97
public ApacheReverseProxy(AwsLandscape<ShardingKey> landscape, AwsInstance<ShardingKey> host) {
... ...
@@ -101,10 +117,35 @@ implements com.sap.sse.landscape.Process<RotatingFileBasedLog, MetricsT> {
101 117
*/
102 118
public void rotateLogs(Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception {
103 119
final String command = "logrotate --force -v /etc/logrotate.d/httpd 2>&1; echo \"logrotate done\"";
104
- logger.info("Standard output from forced log rotate on " + this.getHostname() + ": " + runCommandAndReturnStdoutAndStderr(command, "Standard error from logrotate ",
120
+ logger.info("Standard output from forced log rotate on " + this.getHostname() + ": " + runCommandAndReturnStdoutAndLogStderr(command, "Standard error from logrotate ",
105 121
Level.ALL, optionalKeyName, privateKeyEncryptionPassphrase));
106 122
}
107 123
124
+ @Override
125
+ public Pair<String, String> getArchiveAndFailoverIPs(Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception {
126
+ final String absolute000MacrosConfigFilePath = getAbsoluteConfigFilePath(CONFIG_FILE_FOR_ARCHIVE_AND_FAILOVER_DEFINITION);
127
+ final String command = "cat "+absolute000MacrosConfigFilePath+" | grep \"^Define "+ARCHIVE_IP+"\" | sed -e 's/^Define "+ARCHIVE_IP+" //'; "
128
+ + "cat "+absolute000MacrosConfigFilePath+" | grep \"^Define "+ARCHIVE_FAILOVER_IP+"\" | sed -e 's/^Define "+ARCHIVE_FAILOVER_IP+" //'";
129
+ final String[] archiveAndFailoverIPs = runCommandAndReturnStdoutAndLogStderr(command,
130
+ "Standard error from getting "+ARCHIVE_IP+" and "+ARCHIVE_FAILOVER_IP+": ",
131
+ Level.INFO, optionalKeyName, privateKeyEncryptionPassphrase).split("\n");
132
+ return new Pair<>(archiveAndFailoverIPs[0], archiveAndFailoverIPs[1]);
133
+ }
134
+
135
+ @Override
136
+ public void setArchiveAndFailoverIPs(String productionArchiveIP, String failoverArchiveIP, Optional<String> optionalKeyName,
137
+ byte[] privateKeyEncryptionPassphrase) throws Exception {
138
+ final String absolute000MacrosConfigFilePath = getAbsoluteConfigFilePath(CONFIG_FILE_FOR_ARCHIVE_AND_FAILOVER_DEFINITION);
139
+ final SshCommandChannel sshChannel = getHost().createRootSshChannel(TIMEOUT, optionalKeyName, privateKeyEncryptionPassphrase);
140
+ String patch000MacrosCommand = "su - " + CONFIG_USER + " -c 'cd " + CONFIG_REPO_PATH + " && git checkout "
141
+ + CONFIG_REPO_MAIN_BRANCH_NAME
142
+ + " && sed -i -e \"s/^Define "+ARCHIVE_IP+" .*$/Define "+ARCHIVE_IP+" "+productionArchiveIP+"/\" -e \"s/^Define "+ARCHIVE_FAILOVER_IP+" .*$/Define "+ARCHIVE_FAILOVER_IP+" "+failoverArchiveIP+"/\" "+absolute000MacrosConfigFilePath
143
+ + " && " + createCommitAndPushString(CONFIG_FILE_FOR_ARCHIVE_AND_FAILOVER_DEFINITION, "Switching to new ARCHIVE server", /* performPush */ true)
144
+ + "'"; // concludes the "su"; re-loading is expected to happen through the post-receive hook triggered by the push
145
+ final String stdout = sshChannel.runCommandAndReturnStdoutAndLogStderr(patch000MacrosCommand, "Standard error from switching to new ARCHIVE server", Level.WARNING);
146
+ logger.info("Stdout from upgrading to new ARCHIVE: "+stdout);
147
+ }
148
+
108 149
/**
109 150
* Creates a redirect file and updates the git repo.
110 151
*
... ...
@@ -132,33 +173,33 @@ implements com.sap.sse.landscape.Process<RotatingFileBasedLog, MetricsT> {
132 173
+ CONFIG_REPO_MAIN_BRANCH_NAME + " && echo \"Use " + macroName + " " + hostname + " "
133 174
+ String.join(" ", macroArguments) + "\" > " + getAbsoluteConfigFilePath(configFileNameForHostname);
134 175
if (doCommit) {
135
- command = command + " && cd "
176
+ command = command + " && cd "
136 177
+ CONFIG_REPO_PATH + " && " + createCommitAndPushString(configFileNameForHostname,
137 178
"Set " + configFileNameForHostname + " redirect", doPush);
138 179
}
139 180
command = command + "'; service httpd reload"; // Concludes the su. And reloads as the root user.
140 181
logger.info("Standard output from setting up the re-direct for " + hostname
141 182
+ " and reloading the Apache httpd server: "
142
- + runCommandAndReturnStdoutAndStderr(command,
183
+ + runCommandAndReturnStdoutAndLogStderr(command,
143 184
"Standard error from setting up the re-direct for " + hostname
144 185
+ " and reloading the Apache httpd server: ",
145 186
Level.INFO, optionalKeyName, privateKeyEncryptionPassphrase));
146 187
}
147
-
188
+
148 189
/**
149 190
* Overloads {@link #setRedirect(String, String, String, Optional, byte[], boolean, boolean, String...)} and
150 191
* defaults to {@code true} and {@code true} for committing and pushing.
151 192
*
152 193
* @see #setRedirect(String, String, String, Optional, byte[], boolean, boolean, String...)
153 194
*/
154
- public void setRedirect(String configFileNameForHostname, String macroName, String hostname,
195
+ private void setRedirect(String configFileNameForHostname, String macroName, String hostname,
155 196
Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase, String... macroArguments)
156 197
throws Exception {
157 198
setRedirect(configFileNameForHostname, macroName, hostname, optionalKeyName, privateKeyEncryptionPassphrase,
158 199
/* doCommit */ true, /* doPush */ true, macroArguments);
159 200
}
160 201
161
- private String runCommandAndReturnStdoutAndStderr(String command, String stderrLogPrefix, Level stderrLogLevel,
202
+ private String runCommandAndReturnStdoutAndLogStderr(String command, String stderrLogPrefix, Level stderrLogLevel,
162 203
Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception {
163 204
final SshCommandChannel sshChannel = getHost().createRootSshChannel(TIMEOUT, optionalKeyName, privateKeyEncryptionPassphrase);
164 205
final String stdout = sshChannel.runCommandAndReturnStdoutAndLogStderr(command, stderrLogPrefix, stderrLogLevel);
... ...
@@ -166,10 +207,16 @@ implements com.sap.sse.landscape.Process<RotatingFileBasedLog, MetricsT> {
166 207
}
167 208
168 209
/**
169
- * 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.
170
- * @param editedFileName The file name edited, created or deleted to commit. This includes the {@link #CONFIG_FILE_EXTENSION}, but not a path. The method appends the relative path.
171
- * @param commitMsg The commit message, without escaped speech marks.
172
- * @param performPush Boolean indicating whether to push changes or not. True for performing a push.
210
+ * Creates a command, that can be ran on an instance to commit, and optionally push, changes to a file (within a git
211
+ * repository). ASSUMES the command is ran from within the repository.
212
+ *
213
+ * @param editedFileName
214
+ * The file name edited, created or deleted to commit. This includes the {@link #CONFIG_FILE_EXTENSION},
215
+ * but not a path. The method appends the relative path.
216
+ * @param commitMsg
217
+ * The commit message, without escaped speech marks.
218
+ * @param performPush
219
+ * Boolean indicating whether to push changes or not. True for performing a push.
173 220
* @return Returns the created command (in String form) to perform a commit and optional push.
174 221
*/
175 222
private String createCommitAndPushString(String editedFileName, String commitMsg, boolean performPush) {
... ...
@@ -264,7 +311,6 @@ implements com.sap.sse.landscape.Process<RotatingFileBasedLog, MetricsT> {
264 311
removeRedirect(configFileName, hostname, optionalKeyName, privateKeyEncryptionPassphrase);
265 312
}
266 313
267
-
268 314
/**
269 315
* @param configFileName The name of the file to remove.
270 316
* @param hostname The hostname which was removed.
... ...
@@ -283,7 +329,7 @@ implements com.sap.sse.landscape.Process<RotatingFileBasedLog, MetricsT> {
283 329
command.append("'; service httpd reload;"); // ' closes the su. The reload must be run as the root user.
284 330
logger.info("Standard output from removing the re-direct for " + hostname
285 331
+ " and reloading the Apache httpd server: "
286
- + runCommandAndReturnStdoutAndStderr(command.toString(),
332
+ + runCommandAndReturnStdoutAndLogStderr(command.toString(),
287 333
"Standard error from removing the re-direct for " + hostname
288 334
+ " and reloading the Apache httpd server: ",
289 335
Level.INFO, optionalKeyName, privateKeyEncryptionPassphrase));
java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/ApacheReverseProxyCluster.java
... ...
@@ -14,6 +14,7 @@ import java.util.logging.Logger;
14 14
import com.sap.sse.landscape.aws.LandscapeConstants;
15 15
import com.sap.sse.common.Duration;
16 16
import com.sap.sse.common.Util;
17
+import com.sap.sse.common.Util.Pair;
17 18
import com.sap.sse.concurrent.ConsumerWithException;
18 19
import com.sap.sse.landscape.Landscape;
19 20
import com.sap.sse.landscape.Log;
... ...
@@ -252,4 +253,19 @@ public class ApacheReverseProxyCluster<ShardingKey, MetricsT extends Application
252 253
byte[] privateKeyEncryptionPassphrase) throws Exception {
253 254
setRedirect(proxy -> proxy.removeRedirect(scope, optionalKeyName, privateKeyEncryptionPassphrase));
254 255
}
256
+
257
+ @Override
258
+ public Pair<String, String> getArchiveAndFailoverIPs(Optional<String> optionalKeyName,
259
+ byte[] privateKeyEncryptionPassphrase) throws Exception {
260
+ return getReverseProxies().iterator().next().getArchiveAndFailoverIPs(optionalKeyName, privateKeyEncryptionPassphrase);
261
+ }
262
+
263
+ @Override
264
+ public void setArchiveAndFailoverIPs(String productionArchiveServerInternalIPAddress, String failoverArchiveServerInternalIPAddress,
265
+ Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception {
266
+ if (getReverseProxies().iterator().hasNext()) {
267
+ final ApacheReverseProxy<ShardingKey, MetricsT, ProcessT> proxy = getReverseProxies().iterator().next();
268
+ proxy.setArchiveAndFailoverIPs(productionArchiveServerInternalIPAddress, failoverArchiveServerInternalIPAddress, optionalKeyName, privateKeyEncryptionPassphrase);
269
+ }
270
+ }
255 271
}
java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AwsApplicationProcessImpl.java
... ...
@@ -155,7 +155,7 @@ implements AwsApplicationProcess<ShardingKey, MetricsT, ProcessT> {
155 155
} catch (Exception e) {
156 156
logger.info("Unable to find master by public IP "+ipAddressOrHostname+" ("+e.getMessage()+"); trying to look up master assuming "+ipAddressOrHostname+" is the private IP");
157 157
try {
158
- host = landscape.getHostByPrivateIpAddress(getHost().getRegion(), ipAddressOrHostname, hostSupplier);
158
+ host = landscape.getHostByPrivateDnsNameOrIpAddress(getHost().getRegion(), ipAddressOrHostname, hostSupplier);
159 159
} catch (Exception f) {
160 160
logger.info("Unable to find master by private IP "+ipAddressOrHostname+" ("+f.getMessage()+") either. Returning null.");
161 161
host = null;
java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AwsInstanceImpl.java
... ...
@@ -334,6 +334,11 @@ public class AwsInstanceImpl<ShardingKey> implements AwsInstance<ShardingKey> {
334 334
}
335 335
336 336
@Override
337
+ public void setTerminationProtection(boolean terminationProtection) {
338
+ landscape.setTerminationProtection(this, terminationProtection);
339
+ }
340
+
341
+ @Override
337 342
public void terminate() {
338 343
landscape.terminate(this);
339 344
}
java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AwsLandscapeImpl.java
... ...
@@ -645,24 +645,59 @@ public class AwsLandscapeImpl<ShardingKey> implements AwsLandscape<ShardingKey>
645 645
public <HostT extends AwsInstance<ShardingKey>> HostT getHostByPublicIpAddress(com.sap.sse.landscape.Region region, String publicIpAddress, HostSupplier<ShardingKey, HostT> hostSupplier) {
646 646
return getHost(region, getInstanceByPublicIpAddress(region, publicIpAddress), hostSupplier);
647 647
}
648
-
648
+
649
+ private Instance getFirstInstance(DescribeInstancesResponse response) {
650
+ for (Reservation reservation : response.reservations()) {
651
+ for (Instance instance : reservation.instances()) {
652
+ return instance;
653
+ }
654
+ }
655
+ return null;
656
+ }
657
+
658
+ /**
659
+ * Searches for an instance that matches the private dns name if this fails it tries the to resolve the private ip address
660
+ */
649 661
@Override
650
- public Instance getInstanceByPrivateIpAddress(com.sap.sse.landscape.Region region, String privateIpAddress) {
662
+ public Instance getInstanceByPrivateDnsNameOrIpAddress(com.sap.sse.landscape.Region region, String privateDnsNameOrIpAddress) {
651 663
InetAddress inetAddress;
652 664
try {
653
- inetAddress = InetAddress.getByName(privateIpAddress);
654
- return getEc2Client(getRegion(region))
655
- .describeInstances(b->b.filters(Filter.builder().name("private-ip-address").values(inetAddress.getHostAddress()).build())).reservations()
656
- .iterator().next().instances().iterator().next();
665
+ DescribeInstancesResponse response = getEc2Client(getRegion(region))
666
+ .describeInstances(b -> b.filters(
667
+ Filter.builder().name("private-dns-name").values(privateDnsNameOrIpAddress).build())
668
+ );
669
+ Instance instance = getFirstInstance(response);
670
+ if (instance != null) {
671
+ return instance;
672
+ }
673
+
674
+ inetAddress = InetAddress.getByName(privateDnsNameOrIpAddress);
675
+ response = getEc2Client(getRegion(region))
676
+ .describeInstances(b -> b.filters(
677
+ Filter.builder().name("private-ip-address").values(inetAddress.getHostAddress()).build())
678
+ );
679
+ instance = getFirstInstance(response);
680
+ if (instance != null) {
681
+ return instance;
682
+ }
657 683
} catch (UnknownHostException | NoSuchElementException e) {
658
- logger.warning("IP address for "+privateIpAddress+" not found");
659
- return null;
684
+ logger.severe("An error occurred while trying to find the instance: " + e.getMessage());
660 685
}
661
- }
662 686
687
+ logger.warning("Instance for " + privateDnsNameOrIpAddress + " not found");
688
+ return null;
689
+ }
690
+
691
+ /**
692
+ * Returns a host first by private dns name and alternatively by private ip address
693
+ */
663 694
@Override
664
- public <HostT extends AwsInstance<ShardingKey>> HostT getHostByPrivateIpAddress(com.sap.sse.landscape.Region region, String privateIpAddress, HostSupplier<ShardingKey, HostT> hostSupplier) {
665
- return getHost(region, getInstanceByPrivateIpAddress(region, privateIpAddress), hostSupplier);
695
+ public <HostT extends AwsInstance<ShardingKey>> HostT getHostByPrivateDnsNameOrIpAddress(com.sap.sse.landscape.Region region, String privateIpAddress, HostSupplier<ShardingKey, HostT> hostSupplier) {
696
+ final Instance instanceByPrivateDnsNameOrIpAddress = getInstanceByPrivateDnsNameOrIpAddress(region, privateIpAddress);
697
+ if (instanceByPrivateDnsNameOrIpAddress == null) {
698
+ throw new IllegalArgumentException("Couldn't find instance with IP/hostname "+privateIpAddress+" in region "+region);
699
+ }
700
+ return getHost(region, instanceByPrivateDnsNameOrIpAddress, hostSupplier);
666 701
}
667 702
668 703
private Route53Client getRoute53Client() {
... ...
@@ -1038,6 +1073,21 @@ public class AwsLandscapeImpl<ShardingKey> implements AwsLandscape<ShardingKey>
1038 1073
}
1039 1074
1040 1075
@Override
1076
+ public void setTerminationProtection(AwsInstance<ShardingKey> host, boolean terminationProtection) {
1077
+ logger.info("Setting termination protection for instance "+host+" to "+terminationProtection);
1078
+ getEc2Client(getRegion(host.getAvailabilityZone().getRegion())).modifyInstanceAttribute(b->b
1079
+ .instanceId(host.getInstanceId())
1080
+ .disableApiTermination(a->a.value(terminationProtection)));
1081
+ }
1082
+
1083
+ @Override
1084
+ public void setInstanceName(AwsInstance<ShardingKey> host, String newInstanceName) {
1085
+ logger.info("Setting Name tag for instance "+host+" to "+newInstanceName);
1086
+ getEc2Client(getRegion(host.getAvailabilityZone().getRegion()))
1087
+ .createTags(b -> b.resources(host.getInstanceId()).tags(Tag.builder().key("Name").value(newInstanceName).build()));
1088
+ }
1089
+
1090
+ @Override
1041 1091
public void terminate(AwsInstance<ShardingKey> host) {
1042 1092
logger.info("Terminating instance "+host);
1043 1093
getEc2Client(getRegion(host.getAvailabilityZone().getRegion())).terminateInstances(
... ...
@@ -1195,7 +1245,7 @@ public class AwsLandscapeImpl<ShardingKey> implements AwsLandscape<ShardingKey>
1195 1245
DescribeTargetHealthRequest.builder().targetGroupArn(targetGroup.getTargetGroupArn()).build())
1196 1246
.targetHealthDescriptions().forEach(targetHealthDescription -> {
1197 1247
if (targetHealthDescription.target().id().matches("[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+")) {
1198
- AwsInstance<ShardingKey> awsInstance = getHostByPrivateIpAddress(targetGroup.getRegion(), targetHealthDescription.target().id().trim(),
1248
+ AwsInstance<ShardingKey> awsInstance = getHostByPrivateDnsNameOrIpAddress(targetGroup.getRegion(), targetHealthDescription.target().id().trim(),
1199 1249
AwsInstanceImpl::new);
1200 1250
result.put(awsInstance, targetHealthDescription.targetHealth());
1201 1251
} else {
java/com.sap.sse.landscape/src/com/sap/sse/landscape/Landscape.java
... ...
@@ -13,10 +13,12 @@ public interface Landscape<ShardingKey> {
13 13
* constant ("image-type"). The tag value then must match what the subclass wants.
14 14
*/
15 15
String IMAGE_TYPE_TAG_NAME = "image-type";
16
+
16 17
/**
17 18
* The timeout for a host to come up
18 19
*/
19 20
Optional<Duration> WAIT_FOR_HOST_TIMEOUT = Optional.of(Duration.ONE_HOUR.times(2));
21
+
20 22
/**
21 23
* The timeout for a running process to respond
22 24
*/
java/com.sap.sse.landscape/src/com/sap/sse/landscape/application/ApplicationProcess.java
... ...
@@ -165,6 +165,11 @@ extends Process<RotatingFileBasedLog, MetricsT> {
165 165
return Wait.wait(()->isReady(optionalTimeout), optionalTimeout, Duration.ONE_SECOND.times(5), Level.INFO, ""+this+" not yet ready");
166 166
}
167 167
168
+ default boolean waitUntilAlive(Optional<Duration> optionalTimeout) throws TimeoutException, Exception {
169
+ return Wait.wait(()->isAlive(optionalTimeout), success->success, /* retryOnException */ true,
170
+ optionalTimeout, Duration.ONE_SECOND.times(5), Level.INFO, ""+this+" not yet alive");
171
+ }
172
+
168 173
Release getVersion(Optional<Duration> optionalTimeout, Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception;
169 174
170 175
TimePoint getStartTimePoint(Optional<Duration> optionalTimeout) throws Exception;
java/com.sap.sse.landscape/src/com/sap/sse/landscape/ssh/SshCommandChannelImpl.java
... ...
@@ -37,10 +37,10 @@ public class SshCommandChannelImpl implements SshCommandChannel {
37 37
final ByteArrayOutputStream stderr = new ByteArrayOutputStream();
38 38
try {
39 39
sendCommandLineSynchronously(commandLine, stderr);
40
+ final String result = getStreamContentsAsString();
40 41
if (stderrLogLevel != null && stderr.size() > 0) {
41 42
logger.log(stderrLogLevel, (stderrLogPrefix==null?"":(stderrLogPrefix+": "))+stderr.toString());
42 43
}
43
- final String result = getStreamContentsAsString();
44 44
return result;
45 45
} finally {
46 46
disconnect();
... ...
@@ -51,7 +51,7 @@ public class SshCommandChannelImpl implements SshCommandChannel {
51 51
public InputStream sendCommandLineSynchronously(String commandLine, OutputStream stderr) throws IOException, InterruptedException, JSchException {
52 52
stdout = channel.getInputStream();
53 53
channel.setCommand(commandLine);
54
- channel.setExtOutputStream(stderr);
54
+ channel.setErrStream(stderr);
55 55
channel.connect(/* timeout in milliseconds */ 5000);
56 56
return stdout;
57 57
}
java/com.sap.sse.security/src/com/sap/sse/security/impl/SecurityServiceImpl.java
... ...
@@ -1411,17 +1411,18 @@ implements ReplicableSecurityService, ClearStateTestSupport {
1411 1411
}
1412 1412
1413 1413
/**
1414
- * Schedule a clean-up task to avoid leaking memory for the TimedLock objects; schedule it in two times the
1415
- * locking expiry of {@code timedLock}, but at least one hour, because if no authentication failure occurs
1416
- * for that IP/user agent combination, we will entirely remove the {@link TimedLock} from the map,
1417
- * effectively resetting that IP to a short default locking duration again; this way, if during the double
1418
- * expiration time another failed attempt is registered, we can still grow the locking duration because we have kept
1419
- * the {@link TimedLock} object available for a bit longer. Furthermore, for authentication requests, the
1420
- * responsible {@link Realm} will let authentication requests get to here only if not locked, so if we were to
1421
- * expunge entries immediately as they unlock, the locking duration could never grow.<p>
1414
+ * Schedule a clean-up task to avoid leaking memory for the {@link TimedLock} objects; schedule it in two times the
1415
+ * locking expiry of {@code timedLock}, but at least one hour, because if no authentication failure occurs for that
1416
+ * IP/user agent combination, we will entirely remove the {@link TimedLock} from the map, effectively resetting that
1417
+ * IP to a short default locking duration again; this way, if during the double expiration time another failed
1418
+ * attempt is registered, we can still grow the locking duration because we have kept the {@link TimedLock} object
1419
+ * available for a bit longer. Furthermore, for authentication requests, the responsible {@link Realm} will let
1420
+ * authentication requests get to here only if not locked, so if we were to expunge entries immediately as they
1421
+ * unlock, the locking duration could never grow.
1422
+ * <p>
1422 1423
*
1423
- * With the minimum of one hour, we ensure that failing requests done at a slower rate still grow the locking
1424
- * expiry duration.
1424
+ * With the minimum of one hour, we ensure that failing requests done at a slower rate still grow the locking expiry
1425
+ * duration.
1425 1426
*/
1426 1427
private void scheduleCleanUpTask(final String clientIPOrNull,
1427 1428
final TimedLock timedLock,
java/com.sap.sse.test/src/com/sap/sse/test/ThreadPoolNonDelayedTasksCountTest.java
... ...
@@ -0,0 +1,70 @@
1
+package com.sap.sse.test;
2
+
3
+import static org.junit.jupiter.api.Assertions.assertEquals;
4
+
5
+import java.util.concurrent.ScheduledFuture;
6
+import java.util.concurrent.ScheduledThreadPoolExecutor;
7
+import java.util.concurrent.TimeUnit;
8
+
9
+import org.junit.jupiter.api.AfterEach;
10
+import org.junit.jupiter.api.BeforeEach;
11
+import org.junit.jupiter.api.Test;
12
+
13
+import com.sap.sse.common.Duration;
14
+import com.sap.sse.common.Util;
15
+import com.sap.sse.util.ThreadPoolUtil;
16
+
17
+/**
18
+ * Tests the
19
+ * {@link ThreadPoolUtil#getTasksDelayedByLessThan(java.util.concurrent.ScheduledExecutorService, com.sap.sse.common.Duration)}
20
+ * method.
21
+ *
22
+ * @author Axel Uhl (d043530)
23
+ *
24
+ */
25
+public class ThreadPoolNonDelayedTasksCountTest {
26
+ private ScheduledThreadPoolExecutor defaultBackgroundThreadPoolExecutor;
27
+ private int corePoolSize;
28
+
29
+ @BeforeEach
30
+ public void setUp() {
31
+ defaultBackgroundThreadPoolExecutor = (ScheduledThreadPoolExecutor) ThreadPoolUtil.INSTANCE.getDefaultBackgroundTaskThreadPoolExecutor();
32
+ corePoolSize = defaultBackgroundThreadPoolExecutor.getCorePoolSize();
33
+ }
34
+
35
+ @AfterEach
36
+ public void tearDown() {
37
+ for (final Runnable task : defaultBackgroundThreadPoolExecutor.getQueue()) {
38
+ final ScheduledFuture<?> scheduledFuture = (ScheduledFuture<?>) task;
39
+ scheduledFuture.cancel(/* mayInterruptIfRunning */ true);
40
+ }
41
+ }
42
+
43
+ @Test
44
+ public void testAddingDelayedAndUndelayedThenCounting() throws InterruptedException {
45
+ final int IMMEDIATE_TASK_COUNT = 10000;
46
+ final int DELAYED_TASK_COUNT = 100;
47
+ final Duration DELAY = Duration.ONE_HOUR.times(2);
48
+ for (int i=0; i<IMMEDIATE_TASK_COUNT; i++) {
49
+ defaultBackgroundThreadPoolExecutor.submit(()->{
50
+ try {
51
+ Thread.sleep(IMMEDIATE_TASK_COUNT);
52
+ } catch (InterruptedException e) {
53
+ e.printStackTrace();
54
+ }
55
+ });
56
+ }
57
+ for (int i=0; i<DELAYED_TASK_COUNT; i++) {
58
+ defaultBackgroundThreadPoolExecutor.schedule(()->{
59
+ try {
60
+ Thread.sleep(IMMEDIATE_TASK_COUNT);
61
+ } catch (InterruptedException e) {
62
+ e.printStackTrace();
63
+ }
64
+ }, DELAY.asMillis(), TimeUnit.MILLISECONDS);
65
+ }
66
+ Thread.sleep(100); // wait for non-delayed tasks to get scheduled
67
+ assertEquals(IMMEDIATE_TASK_COUNT-corePoolSize, Util.size(ThreadPoolUtil.INSTANCE.getTasksDelayedByLessThan(defaultBackgroundThreadPoolExecutor, DELAY.divide(2))));
68
+ assertEquals(IMMEDIATE_TASK_COUNT+DELAYED_TASK_COUNT-corePoolSize, Util.size(ThreadPoolUtil.INSTANCE.getTasksDelayedByLessThan(defaultBackgroundThreadPoolExecutor, DELAY.times(2))));
69
+ }
70
+}
java/com.sap.sse/src/com/sap/sse/util/ThreadPoolUtil.java
... ...
@@ -5,9 +5,12 @@ import java.util.concurrent.Callable;
5 5
import java.util.concurrent.ExecutorService;
6 6
import java.util.concurrent.Future;
7 7
import java.util.concurrent.ScheduledExecutorService;
8
+import java.util.concurrent.ScheduledFuture;
8 9
import java.util.concurrent.ScheduledThreadPoolExecutor;
10
+import java.util.concurrent.ThreadPoolExecutor;
9 11
import java.util.logging.Level;
10 12
13
+import com.sap.sse.common.Duration;
11 14
import com.sap.sse.util.impl.ThreadPoolUtilImpl;
12 15
13 16
public interface ThreadPoolUtil {
... ...
@@ -118,4 +121,11 @@ public interface ThreadPoolUtil {
118 121
Runnable associateWithSubjectIfAny(Runnable runnable);
119 122
120 123
<T> Callable<T> associateWithSubjectIfAny(Callable<T> callable);
124
+
125
+ /**
126
+ * In the {@code executor}'s queue filters tasks for those with a delay less than {@code delayLessThan} and
127
+ * returns the corresponding tasks. This can be used, e.g., to judge an executor's immediate workload or
128
+ * give an estimate of the future workload mapped over time.
129
+ */
130
+ Iterable<ScheduledFuture<?>> getTasksDelayedByLessThan(ThreadPoolExecutor executor, Duration delayLessThan);
121 131
}
java/com.sap.sse/src/com/sap/sse/util/impl/ThreadPoolUtilImpl.java
... ...
@@ -8,6 +8,9 @@ import java.util.concurrent.ExecutionException;
8 8
import java.util.concurrent.ExecutorService;
9 9
import java.util.concurrent.Future;
10 10
import java.util.concurrent.ScheduledExecutorService;
11
+import java.util.concurrent.ScheduledFuture;
12
+import java.util.concurrent.ThreadPoolExecutor;
13
+import java.util.concurrent.TimeUnit;
11 14
import java.util.logging.Level;
12 15
import java.util.logging.Logger;
13 16
... ...
@@ -15,6 +18,7 @@ import org.apache.shiro.SecurityUtils;
15 18
import org.apache.shiro.UnavailableSecurityManagerException;
16 19
import org.apache.shiro.subject.Subject;
17 20
21
+import com.sap.sse.common.Duration;
18 22
import com.sap.sse.common.Util;
19 23
import com.sap.sse.util.ThreadPoolUtil;
20 24
... ...
@@ -130,4 +134,11 @@ public class ThreadPoolUtilImpl implements ThreadPoolUtil {
130 134
public <T> Callable<T> associateWithSubjectIfAny(Callable<T> callable) {
131 135
return getSubjectOrNull().map(subject->subject.associateWith(callable)).orElse(callable);
132 136
}
137
+
138
+ @Override
139
+ public Iterable<ScheduledFuture<?>> getTasksDelayedByLessThan(ThreadPoolExecutor executor,
140
+ Duration delayLessThan) {
141
+ return Util.map(Util.filter(executor.getQueue(), task->((ScheduledFuture<?>) task).getDelay(TimeUnit.MILLISECONDS) < delayLessThan.asMillis()),
142
+ task->(ScheduledFuture<?>) task);
143
+ }
133 144
}