Files
sailing-analytics/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/impl/LandscapeServiceImpl.java
T

1971 lines
144 KiB
Java

package com.sap.sailing.landscape.impl;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.http.client.ClientProtocolException;
import org.apache.shiro.authz.AuthorizationException;
import org.json.simple.parser.ParseException;
import org.osgi.framework.BundleContext;
import org.osgi.util.tracker.ServiceTracker;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.SftpException;
import com.sap.sailing.domain.common.DataImportProgress;
import com.sap.sailing.landscape.ALBToReverseProxyArchiveRedirectMapper;
import com.sap.sailing.landscape.AwsSessionCredentialsWithExpiry;
import com.sap.sailing.landscape.EligibleInstanceForReplicaSetFindingStrategy;
import com.sap.sailing.landscape.LandscapeService;
import com.sap.sailing.landscape.SailingAnalyticsHost;
import com.sap.sailing.landscape.SailingAnalyticsMetrics;
import com.sap.sailing.landscape.SailingAnalyticsProcess;
import com.sap.sailing.landscape.SailingReleaseRepository;
import com.sap.sailing.landscape.common.RemoteServiceMappingConstants;
import com.sap.sailing.landscape.common.SharedLandscapeConstants;
import com.sap.sailing.landscape.procedures.CreateLaunchConfigurationAndAutoScalingGroup;
import com.sap.sailing.landscape.procedures.DeployProcessOnMultiServer;
import com.sap.sailing.landscape.procedures.SailingAnalyticsHostSupplier;
import com.sap.sailing.landscape.procedures.SailingAnalyticsMasterConfiguration;
import com.sap.sailing.landscape.procedures.SailingAnalyticsProcessFactory;
import com.sap.sailing.landscape.procedures.SailingAnalyticsReplicaConfiguration;
import com.sap.sailing.landscape.procedures.SailingAnalyticsReplicaConfiguration.Builder;
import com.sap.sailing.landscape.procedures.SailingProcessConfigurationVariables;
import com.sap.sailing.landscape.procedures.StartMultiServer;
import com.sap.sailing.landscape.procedures.StartSailingAnalyticsHost;
import com.sap.sailing.landscape.procedures.StartSailingAnalyticsMasterHost;
import com.sap.sailing.landscape.procedures.StartSailingAnalyticsReplicaHost;
import com.sap.sailing.server.gateway.interfaces.CompareServersResult;
import com.sap.sailing.server.gateway.interfaces.MasterDataImportResult;
import com.sap.sailing.server.gateway.interfaces.SailingServer;
import com.sap.sailing.server.gateway.interfaces.SailingServerFactory;
import com.sap.sse.ServerInfo;
import com.sap.sse.common.Duration;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.Util;
import com.sap.sse.common.Util.Pair;
import com.sap.sse.common.Util.Triple;
import com.sap.sse.common.mail.MailException;
import com.sap.sse.i18n.ResourceBundleStringMessages;
import com.sap.sse.i18n.impl.ResourceBundleStringMessagesImpl;
import com.sap.sse.landscape.DefaultProcessConfigurationVariables;
import com.sap.sse.landscape.InboundReplicationConfiguration;
import com.sap.sse.landscape.Landscape;
import com.sap.sse.landscape.Release;
import com.sap.sse.landscape.RotatingFileBasedLog;
import com.sap.sse.landscape.application.ProcessFactory;
import com.sap.sse.landscape.aws.AmazonMachineImage;
import com.sap.sse.landscape.aws.ApplicationLoadBalancer;
import com.sap.sse.landscape.aws.ApplicationProcessHost;
import com.sap.sse.landscape.aws.AwsApplicationReplicaSet;
import com.sap.sse.landscape.aws.AwsAutoScalingGroup;
import com.sap.sse.landscape.aws.AwsAvailabilityZone;
import com.sap.sse.landscape.aws.AwsInstance;
import com.sap.sse.landscape.aws.AwsLandscape;
import com.sap.sse.landscape.aws.AwsShard;
import com.sap.sse.landscape.aws.HostSupplier;
import com.sap.sse.landscape.aws.ReverseProxy;
import com.sap.sse.landscape.aws.Tags;
import com.sap.sse.landscape.aws.TargetGroup;
import com.sap.sse.landscape.aws.common.shared.RedirectDTO;
import com.sap.sse.landscape.aws.impl.AwsApplicationReplicaSetImpl;
import com.sap.sse.landscape.aws.impl.AwsRegion;
import com.sap.sse.landscape.aws.impl.DNSCache;
import com.sap.sse.landscape.aws.orchestration.AddShardingKeyToShard;
import com.sap.sse.landscape.aws.orchestration.AwsApplicationConfiguration;
import com.sap.sse.landscape.aws.orchestration.CopyAndCompareMongoDatabase;
import com.sap.sse.landscape.aws.orchestration.CreateDNSBasedLoadBalancerMapping;
import com.sap.sse.landscape.aws.orchestration.CreateDynamicLoadBalancerMapping;
import com.sap.sse.landscape.aws.orchestration.CreateLoadBalancerMapping;
import com.sap.sse.landscape.aws.orchestration.CreateShard;
import com.sap.sse.landscape.aws.orchestration.RemoveShardingKeyFromShard;
import com.sap.sse.landscape.aws.orchestration.ShardProcedure;
import com.sap.sse.landscape.aws.orchestration.StartAwsHost;
import com.sap.sse.landscape.mongodb.Database;
import com.sap.sse.landscape.mongodb.MongoEndpoint;
import com.sap.sse.replication.FullyInitializedReplicableTracker;
import com.sap.sse.security.SecurityService;
import com.sap.sse.security.SessionUtils;
import com.sap.sse.security.shared.HasPermissions.Action;
import com.sap.sse.security.shared.HasPermissions.DefaultActions;
import com.sap.sse.security.shared.OwnershipAnnotation;
import com.sap.sse.security.shared.QualifiedObjectIdentifier;
import com.sap.sse.security.shared.ServerAdminRole;
import com.sap.sse.security.shared.TypeRelativeObjectIdentifier;
import com.sap.sse.security.shared.WildcardPermission;
import com.sap.sse.security.shared.impl.SecuredSecurityTypes;
import com.sap.sse.security.shared.impl.SecuredSecurityTypes.ServerActions;
import com.sap.sse.security.shared.impl.User;
import com.sap.sse.security.util.RemoteServerUtil;
import com.sap.sse.shared.util.Wait;
import com.sap.sse.util.JvmUtils;
import com.sap.sse.util.ServiceTrackerFactory;
import com.sap.sse.util.ThreadPoolUtil;
import software.amazon.awssdk.services.autoscaling.model.AutoScalingGroup;
import software.amazon.awssdk.services.autoscaling.model.LaunchConfiguration;
import software.amazon.awssdk.services.ec2.model.InstanceType;
import software.amazon.awssdk.services.elasticloadbalancingv2.model.Listener;
import software.amazon.awssdk.services.elasticloadbalancingv2.model.Rule;
import software.amazon.awssdk.services.elasticloadbalancingv2.model.TargetHealthDescription;
import software.amazon.awssdk.services.route53.model.RRType;
import software.amazon.awssdk.services.route53.model.ResourceRecordSet;
import software.amazon.awssdk.services.sts.model.Credentials;
public class LandscapeServiceImpl implements LandscapeService {
private static final Logger logger = Logger.getLogger(LandscapeServiceImpl.class.getName());
private static final String STRING_MESSAGES_BASE_NAME = "stringmessages/SailingLandscape_StringMessages";
private static final String TEMPORARY_UPGRADE_REPLICA_NAME_SUFFIX = " (Upgrade Replica)";
private static final String UPGRADE_REPLICA_TAG_KEY = "upgradeReplica";
private final FullyInitializedReplicableTracker<SecurityService> securityServiceTracker;
private final ProcessFactory<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>, SailingAnalyticsHost<String>> processFactoryFromHostAndServerDirectory;
private final ServiceTracker<SailingServerFactory, SailingServerFactory> sailingServerFactoryTracker;
public LandscapeServiceImpl(BundleContext context) {
securityServiceTracker = FullyInitializedReplicableTracker.createAndOpen(context, SecurityService.class);
sailingServerFactoryTracker = ServiceTrackerFactory.createAndOpen(context, SailingServerFactory.class);
processFactoryFromHostAndServerDirectory =
(host, port, serverDirectory, telnetPort, serverName, additionalProperties)->{
try {
final Number expeditionUdpPort = (Number) additionalProperties.get(SailingProcessConfigurationVariables.EXPEDITION_PORT.name());
return new SailingAnalyticsProcessImpl<String>(port, host, serverDirectory, telnetPort, serverName,
expeditionUdpPort == null ? null : expeditionUdpPort.intValue(), getLandscape());
} catch (Exception e) {
throw new RuntimeException(e);
}
};
}
@Override
public AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> createApplicationReplicaSet(
String regionId, String name, boolean newSharedMasterInstance, String sharedInstanceType,
String dedicatedInstanceType, boolean dynamicLoadBalancerMapping, String releaseNameOrNullForLatestMaster,
String optionalKeyName, byte[] privateKeyEncryptionPassphrase, String masterReplicationBearerToken,
String replicaReplicationBearerToken, String optionalDomainName, Integer optionalMemoryInMegabytesOrNull,
Integer optionalMemoryTotalSizeFactorOrNull, Optional<Integer> minimumAutoScalingGroupSize,
Optional<Integer> maximumAutoScalingGroupSize) throws Exception {
final AwsLandscape<String> landscape = getLandscape();
final String hostname = getHostname(name, optionalDomainName);
final Iterable<ResourceRecordSet> existingDNSRulesForHostname = landscape.getResourceRecordSets(hostname);
// Failing early in case DNS record already exists (see also bug 5826):
if (existingDNSRulesForHostname != null && !Util.isEmpty(existingDNSRulesForHostname)) {
throw new IllegalArgumentException("DNS record for "+hostname+" already exists");
}
final AwsRegion region = new AwsRegion(regionId, landscape);
final Release release = getRelease(releaseNameOrNullForLatestMaster);
final com.sap.sailing.landscape.procedures.SailingAnalyticsMasterConfiguration.Builder<?, String> masterConfigurationBuilder =
createMasterConfigurationBuilder(name, masterReplicationBearerToken, optionalMemoryInMegabytesOrNull,
newSharedMasterInstance ? optionalMemoryTotalSizeFactorOrNull : null, region, release);
final String bearerTokenUsedByReplicas = getEffectiveBearerToken(replicaReplicationBearerToken);
final InboundReplicationConfiguration inboundMasterReplicationConfiguration = masterConfigurationBuilder.getInboundReplicationConfiguration().get();
establishServerGroupAndTryToMakeCurrentUserItsOwnerAndMember(name, bearerTokenUsedByReplicas,
inboundMasterReplicationConfiguration.getMasterHostname(), inboundMasterReplicationConfiguration.getMasterHttpPort());
final com.sap.sailing.landscape.procedures.StartSailingAnalyticsMasterHost.Builder<?, String> masterHostBuilder = StartSailingAnalyticsMasterHost.masterHostBuilder(masterConfigurationBuilder);
masterHostBuilder
.setInstanceType(InstanceType.valueOf(newSharedMasterInstance ? sharedInstanceType : dedicatedInstanceType))
.setOptionalTimeout(Landscape.WAIT_FOR_HOST_TIMEOUT)
.setLandscape(landscape)
.setRegion(region)
.setPrivateKeyEncryptionPassphrase(privateKeyEncryptionPassphrase);
if (optionalKeyName != null) {
masterHostBuilder.setKeyName(optionalKeyName);
}
if (newSharedMasterInstance) {
masterHostBuilder
.setInstanceName(SharedLandscapeConstants.MULTI_PROCESS_INSTANCE_DEFAULT_NAME)
.setTags(Tags.with(SharedLandscapeConstants.SAILING_ANALYTICS_APPLICATION_HOST_TAG, SharedLandscapeConstants.MULTI_PROCESS_INSTANCE_TAG_VALUE));
}
final StartSailingAnalyticsMasterHost<String> masterHostStartProcedure = masterHostBuilder.build();
masterHostStartProcedure.run();
final SailingAnalyticsProcess<String> master = masterHostStartProcedure.getSailingAnalyticsProcess();
final AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> result =
createLoadBalancingAndAutoScalingSetup(landscape, region, name, master, release, dedicatedInstanceType,
dynamicLoadBalancerMapping, optionalKeyName, privateKeyEncryptionPassphrase, optionalDomainName,
Optional.of(masterHostBuilder.getMachineImage()), bearerTokenUsedByReplicas,
minimumAutoScalingGroupSize, maximumAutoScalingGroupSize);
final Optional<SailingAnalyticsProcess<String>> unmanagedReplica = minimumAutoScalingGroupSize.map(minASGSize->{
final List<SailingAnalyticsProcess<String>> unmanagedReplicas = new ArrayList<>();
if (minASGSize == 0) {
logger.info("No auto-scaling replica forced for replica set "+name+"; starting with an unmanaged replica on a shared instance");
try {
unmanagedReplicas.add(launchUnmanagedReplica(result, region, optionalKeyName, privateKeyEncryptionPassphrase,
bearerTokenUsedByReplicas, optionalMemoryInMegabytesOrNull, optionalMemoryTotalSizeFactorOrNull,
Optional.of(InstanceType.valueOf(sharedInstanceType)),
/* optionalPreferredInstanceToDeployTo */ Optional.empty()));
} catch (Exception e) {
throw new RuntimeException(e);
}
};
return unmanagedReplicas.isEmpty() ? null : unmanagedReplicas.get(0);
});
// if an unmanaged replica process was launched, return a replica set that contains it; otherwise use the one we already have (without any replica)
return unmanagedReplica.map(ur->{
try {
return getLandscape().getApplicationReplicaSet(region, name, master, Collections.singleton(ur));
} catch (InterruptedException | ExecutionException | TimeoutException e) {
throw new RuntimeException(e);
}
}).orElse(result);
}
@Override
public AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> deployApplicationToExistingHost(String replicaSetName,
SailingAnalyticsHost<String> hostToDeployTo, String replicaInstanceType, boolean dynamicLoadBalancerMapping,
String releaseNameOrNullForLatestMaster, String optionalKeyName, byte[] privateKeyEncryptionPassphrase,
String masterReplicationBearerToken, String replicaReplicationBearerToken,
String optionalDomainName, Optional<Integer> optionalMinimumAutoScalingGroupSize, Optional<Integer> optionalMaximumAutoScalingGroupSize,
Integer optionalMemoryInMegabytesOrNull,
Integer optionalMemoryTotalSizeFactorOrNull, Optional<InstanceType> optionalSharedInstanceTypeForNewReplicaHost, Optional<SailingAnalyticsHost<String>> optionalPreferredInstanceToDeployUnmanagedReplicaTo) throws Exception {
return deployApplicationToExistingHostInternal(hostToDeployTo.getRegion(),
replicaSetName, hostToDeployTo,
replicaInstanceType, dynamicLoadBalancerMapping, releaseNameOrNullForLatestMaster, optionalKeyName,
privateKeyEncryptionPassphrase, masterReplicationBearerToken, replicaReplicationBearerToken,
optionalDomainName, optionalMinimumAutoScalingGroupSize, optionalMaximumAutoScalingGroupSize,
optionalMemoryInMegabytesOrNull, optionalMemoryTotalSizeFactorOrNull, optionalSharedInstanceTypeForNewReplicaHost, optionalPreferredInstanceToDeployUnmanagedReplicaTo);
}
@Override
public <AppConfigBuilderT extends SailingAnalyticsReplicaConfiguration.Builder<AppConfigBuilderT, String>,
MultiServerDeployerBuilderT extends DeployProcessOnMultiServer.Builder<MultiServerDeployerBuilderT, String, SailingAnalyticsHost<String>, SailingAnalyticsReplicaConfiguration<String>, AppConfigBuilderT>>
SailingAnalyticsProcess<String> deployReplicaToExistingHost(final AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet,
SailingAnalyticsHost<String> hostToDeployTo, String optionalKeyName, byte[] privateKeyEncryptionPassphrase, String replicaReplicationBearerToken,
Integer optionalMemoryInMegabytesOrNull, Integer optionalMemoryTotalSizeFactorOrNull) throws Exception {
logger.info("Deploying replica for application replica set "+replicaSet.getName()+" to host "+hostToDeployTo);
return spinUpReplicaAndRegisterInPublicTargetGroup(hostToDeployTo.getRegion(), replicaSet, Optional.ofNullable(optionalKeyName), privateKeyEncryptionPassphrase, replicaReplicationBearerToken,
/* processLauncher: */ (AppConfigBuilderT replicaConfigurationBuilder)->{
if (optionalMemoryInMegabytesOrNull != null) {
replicaConfigurationBuilder.setMemoryInMegabytes(optionalMemoryInMegabytesOrNull);
} else if (optionalMemoryTotalSizeFactorOrNull != null) {
replicaConfigurationBuilder.setMemoryTotalSizeFactor(optionalMemoryTotalSizeFactorOrNull);
}
// the process launcher uses the DeployProcessOnMultiServer procedure to launch the process based on the replica config
final DeployProcessOnMultiServer.Builder<MultiServerDeployerBuilderT, String, SailingAnalyticsHost<String>, SailingAnalyticsReplicaConfiguration<String>, AppConfigBuilderT> replicaDeploymentProcessBuilder =
DeployProcessOnMultiServer.<MultiServerDeployerBuilderT, String, SailingAnalyticsHost<String>, SailingAnalyticsReplicaConfiguration<String>, AppConfigBuilderT> builder(replicaConfigurationBuilder, hostToDeployTo);
if (optionalKeyName != null) {
replicaDeploymentProcessBuilder.setKeyName(optionalKeyName);
}
replicaDeploymentProcessBuilder
.setOptionalTimeout(Landscape.WAIT_FOR_PROCESS_TIMEOUT)
.setPrivateKeyEncryptionPassphrase(privateKeyEncryptionPassphrase);
final DeployProcessOnMultiServer<String, SailingAnalyticsHost<String>, SailingAnalyticsReplicaConfiguration<String>, AppConfigBuilderT> replicaDeploymentProcess;
try {
replicaDeploymentProcess = replicaDeploymentProcessBuilder.build();
replicaDeploymentProcess.run();
return replicaDeploymentProcess.getProcess();
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
/**
* Starts a first master process of a new replica set whose name is provided by the {@code replicaSetName}
* parameter. The process is started on the host identified by the {@code hostToDeployTo} parameter. A set of
* available ports is identified and chosen automatically. The target groups and load balancing set-up is created.
* The {@code replicaInstanceType} is used to configure the launch configuration used by the auto-scaling group
* which is also created so that when dedicated replicas need to be provided during auto-scaling, their instance
* type is known.
* <p>
*
* The "internal" method exists in order to declare a few type parameters which wouldn't be possible on the GWT RPC
* interface method as some of these types are not seen by clients.
* @param optionalMinimumAutoScalingGroupSize
* defaults to 1; if 0, a replica process will be launched on an eligible shared instance in an
* availability zone different from that of the instance hosting the master process. Otherwise,
* at least one auto-scaling replica will ensure availability of the replica set.
* @param optionalInstanceType
* if a new instance must be launched because no eligible one is found, this parameter can be used to
* specify its instance type. It defaults to {@link InstanceType#I3_2_XLARGE} which is reasonably suited
* for a multi-process set-up.
* @param optionalPreferredInstanceToDeployTo
* If {@link Optional#isPresent() present}, specifies a preferred host for the answer given by
* {@link #getInstanceToDeployTo(AwsApplicationReplicaSet)}. However, if the instance turns out not to be
* eligible by the rules regarding general
* {@link AwsApplicationReplicaSet#isEligibleForDeployment(ApplicationProcessHost, Optional, Optional, byte[])
* eligibility} (based mainly on port and directory available as well as not being managed by an
* auto-scaling group) and additional rules regarding availability zone "anti-affinity" as defined here
* (see {@link #master}), the default rules for selecting or launching an eligible instance apply.
*/
private <AppConfigBuilderT extends SailingAnalyticsMasterConfiguration.Builder<AppConfigBuilderT, String>,
MultiServerDeployerBuilderT extends DeployProcessOnMultiServer.Builder<MultiServerDeployerBuilderT, String, SailingAnalyticsHost<String>, SailingAnalyticsMasterConfiguration<String>, AppConfigBuilderT>>
AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> deployApplicationToExistingHostInternal(
AwsRegion region, String replicaSetName, SailingAnalyticsHost<String> hostToDeployTo,
String replicaInstanceType, boolean dynamicLoadBalancerMapping,
String releaseNameOrNullForLatestMaster, String optionalKeyName,
byte[] privateKeyEncryptionPassphrase, String masterReplicationBearerToken,
String replicaReplicationBearerToken, String optionalDomainName,
Optional<Integer> optionalMinimumAutoScalingGroupSize,
Optional<Integer> optionalMaximumAutoScalingGroupSize, Integer optionalMemoryInMegabytesOrNull,
Integer optionalMemoryTotalSizeFactorOrNull, Optional<InstanceType> optionalInstanceType,
Optional<SailingAnalyticsHost<String>> optionalPreferredInstanceToDeployTo) throws Exception {
final AwsLandscape<String> landscape = getLandscape();
final String hostname = getHostname(replicaSetName, optionalDomainName);
final Iterable<ResourceRecordSet> existingDNSRulesForHostname = landscape.getResourceRecordSets(hostname);
// Failing early in case DNS record already exists (see also bug 5826):
if (existingDNSRulesForHostname != null && !Util.isEmpty(existingDNSRulesForHostname)) {
throw new IllegalArgumentException("DNS record for "+hostname+" already exists");
}
final Release release = getRelease(releaseNameOrNullForLatestMaster);
final AppConfigBuilderT masterConfigurationBuilder = createMasterConfigurationBuilder(replicaSetName,
masterReplicationBearerToken, optionalMemoryInMegabytesOrNull, optionalMemoryTotalSizeFactorOrNull,
region, release);
final InboundReplicationConfiguration inboundMasterReplicationConfiguration = masterConfigurationBuilder.getInboundReplicationConfiguration().get();
final String bearerTokenUsedByReplicas = getEffectiveBearerToken(replicaReplicationBearerToken);
establishServerGroupAndTryToMakeCurrentUserItsOwnerAndMember(replicaSetName, bearerTokenUsedByReplicas,
inboundMasterReplicationConfiguration.getMasterHostname(), inboundMasterReplicationConfiguration.getMasterHttpPort());
final SailingAnalyticsProcess<String> master = deployProcessToSharedInstance(hostToDeployTo,
masterConfigurationBuilder, optionalKeyName, privateKeyEncryptionPassphrase);
final AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet =
createLoadBalancingAndAutoScalingSetup(landscape, region, replicaSetName, master, release, replicaInstanceType, dynamicLoadBalancerMapping,
optionalKeyName, privateKeyEncryptionPassphrase, optionalDomainName, /* use default AMI as replica machine image */ Optional.empty(),
bearerTokenUsedByReplicas, optionalMinimumAutoScalingGroupSize, optionalMaximumAutoScalingGroupSize);
final Iterable<SailingAnalyticsProcess<String>> replicas;
if (optionalMinimumAutoScalingGroupSize.isPresent() && optionalMinimumAutoScalingGroupSize.get() == 0) {
replicas = Collections.singleton(launchUnmanagedReplica(replicaSet, region, optionalKeyName,
privateKeyEncryptionPassphrase, bearerTokenUsedByReplicas, optionalMemoryInMegabytesOrNull,
optionalMemoryTotalSizeFactorOrNull, optionalInstanceType, optionalPreferredInstanceToDeployTo));
} else {
replicas = Collections.emptySet();
}
final AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSetWithReplica =
landscape.getApplicationReplicaSet(region, replicaSet.getServerName(), master, replicas);
return replicaSetWithReplica;
}
private <AppConfigBuilderT extends SailingAnalyticsMasterConfiguration.Builder<AppConfigBuilderT, String>,
MultiServerDeployerBuilderT extends DeployProcessOnMultiServer.Builder<MultiServerDeployerBuilderT, String, SailingAnalyticsHost<String>, SailingAnalyticsMasterConfiguration<String>, AppConfigBuilderT>>
SailingAnalyticsProcess<String> deployProcessToSharedInstance(
SailingAnalyticsHost<String> hostToDeployTo, final AppConfigBuilderT applicationConfigurationBuilder,
String optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception {
final DeployProcessOnMultiServer.Builder<MultiServerDeployerBuilderT, String, SailingAnalyticsHost<String>, SailingAnalyticsMasterConfiguration<String>, AppConfigBuilderT> multiServerAppDeployerBuilder =
DeployProcessOnMultiServer.<MultiServerDeployerBuilderT, String, SailingAnalyticsHost<String>, SailingAnalyticsMasterConfiguration<String>, AppConfigBuilderT> builder(
applicationConfigurationBuilder, hostToDeployTo);
if (optionalKeyName != null) {
multiServerAppDeployerBuilder.setKeyName(optionalKeyName);
}
multiServerAppDeployerBuilder
.setPrivateKeyEncryptionPassphrase(privateKeyEncryptionPassphrase)
.setOptionalTimeout(Landscape.WAIT_FOR_HOST_TIMEOUT);
final DeployProcessOnMultiServer<String, SailingAnalyticsHost<String>, SailingAnalyticsMasterConfiguration<String>, AppConfigBuilderT> deployer = multiServerAppDeployerBuilder.build();
deployer.run();
final SailingAnalyticsProcess<String> master = deployer.getProcess();
return master;
}
/**
* Starts a replica process for the given {@code replicaSet}. The memory configuration can optionally be defined. It
* defaults to the usual large share of the physical RAM of the instance it is deployed on. The replica is launched
* on the {@code optionalPreferredInstanceToDeployTo} host if specified and eligible, else on the "best" existing
* eligible instance (based on aspects such as the processes already deployed on the instance). If no running
* instance is found to be eligible (e.g., because none has the port required by the replica available, or all run
* in the same availability zone as the replica set's master process), a new instance is launched.
* <p>
*
* If a new instance needs to be launched, the instance type can optionally be specified. It defaults to something
* with large fast swap space and a reasonable amount of physical RAM that would allow somewhere between two and
* four processes to share the physical RAM even during a live event.
*
* @param optionalInstanceType
* defaults to {@link SharedLandscapeConstants#DEFAULT_SHARED_INSTANCE_TYPE_NAME}
*/
private SailingAnalyticsProcess<String> launchUnmanagedReplica(
final AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet,
AwsRegion region, String optionalKeyName, byte[] privateKeyEncryptionPassphrase,
String replicaReplicationBearerToken, Integer optionalMemoryInMegabytesOrNull,
Integer optionalMemoryTotalSizeFactorOrNull, Optional<InstanceType> optionalInstanceType,
Optional<SailingAnalyticsHost<String>> optionalPreferredInstanceToDeployTo) throws Exception {
final EligibleInstanceForReplicaSetFindingStrategy strategyForFindingOrLaunchingInstanceForUnmangedReplica =
new EligibleInstanceForReplicaSetFindingStrategyImpl(this, region, optionalKeyName,
privateKeyEncryptionPassphrase, /* master==false because we'd like to deploy a replica */ false,
/* mustBeDifferentAvailabilityZone */ true, optionalInstanceType,
optionalPreferredInstanceToDeployTo);
final SailingAnalyticsProcess<String> replica = deployReplicaToExistingHost(
replicaSet, strategyForFindingOrLaunchingInstanceForUnmangedReplica.getInstanceToDeployTo(replicaSet),
optionalKeyName, privateKeyEncryptionPassphrase, replicaReplicationBearerToken, optionalMemoryInMegabytesOrNull, optionalMemoryTotalSizeFactorOrNull);
return replica;
}
@Override
public Util.Triple<DataImportProgress, CompareServersResult, String> archiveReplicaSet(String regionId,
AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> applicationReplicaSetToArchive,
String bearerTokenOrNullForApplicationReplicaSetToArchive,
String bearerTokenOrNullForArchive,
Duration durationToWaitBeforeCompareServers,
int maxNumberOfCompareServerAttempts, boolean removeApplicationReplicaSet, MongoEndpoint moveDatabaseHere,
String optionalKeyName, byte[] passphraseForPrivateKeyDecryption)
throws Exception {
if (removeApplicationReplicaSet && applicationReplicaSetToArchive.isLocalReplicaSet()) {
throw new IllegalArgumentException("A replica set cannot archive itself if it is going to be removed. Current replica set: "+ServerInfo.getName());
}
final AwsRegion region = new AwsRegion(regionId, getLandscape());
final AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> archiveReplicaSet = getApplicationReplicaSet(
region, SharedLandscapeConstants.ARCHIVE_SERVER_APPLICATION_REPLICA_SET_NAME,
Landscape.WAIT_FOR_PROCESS_TIMEOUT.get().asMillis(), optionalKeyName,
passphraseForPrivateKeyDecryption);
if (archiveReplicaSet == null) {
final String msg = "Couldn't find archive replica set tagged as "+SharedLandscapeConstants.ARCHIVE_SERVER_APPLICATION_REPLICA_SET_NAME;
logger.severe(msg);
throw new IllegalArgumentException(msg);
}
logger.info("Found ARCHIVE replica set " + archiveReplicaSet + " with master " + archiveReplicaSet.getMaster());
final UUID idForProgressTracking = UUID.randomUUID();
final RedirectDTO defaultRedirect = RedirectDTO.from(getDefaultRedirectPath(applicationReplicaSetToArchive.getDefaultRedirectRule()));
final String hostnameFromWhichToArchive = applicationReplicaSetToArchive.getHostname();
final String hostnameOfArchive = archiveReplicaSet.getHostname();
final SailingServerFactory sailingServerFactory = sailingServerFactoryTracker.getService();
if (sailingServerFactory == null) {
throw new IllegalStateException("Couldn't find SailingServerFactory");
}
final SailingServer from = sailingServerFactory.getSailingServer(new URL("https", hostnameFromWhichToArchive, "/"), bearerTokenOrNullForApplicationReplicaSetToArchive);
final SailingServer archive = sailingServerFactory.getSailingServer(new URL("https", hostnameOfArchive, "/"), bearerTokenOrNullForArchive);
logger.info("Importing master data from "+from+" to "+archive);
sendMailToReplicaSetOwner(archiveReplicaSet, "StartingToArchiveReplicaSetIntoSubject", "StartingToArchiveReplicaSetIntoBody", Optional.of(ServerActions.CAN_IMPORT_MASTERDATA));
sendMailToReplicaSetOwner(applicationReplicaSetToArchive, "StartingToArchiveReplicaSetSubject", "StartingToArchiveReplicaSetBody", Optional.of(ServerActions.CAN_EXPORT_MASTERDATA));
// Note: if from.getLeaderboardGroupIds() returns an empty set, "all" leaderboards will be imported by the MDI which again is the empty set.
// In this case, no comparison is required; in fact it wouldn't even work because passing an empty set to the archive into which the import
// was done would implicitly compare all leaderboard groups, resulting in the entire archive server content being the "diff."
final MasterDataImportResult mdiResult = archive.importMasterData(from, from.getLeaderboardGroupIds(), /* override */ true, /* compress */ true,
/* import wind */ true, /* import device configurations */ false, /* import tracked races and start tracking */ true, Optional.of(idForProgressTracking));
if (mdiResult == null) {
logger.severe("Couldn't find any result for the master data import. Aborting.");
throw new IllegalStateException("Couldn't find any result for the master data import. Aborting archiving of replica set "+from);
}
final DataImportProgress mdiProgress = waitForMDICompletionOrError(archive, idForProgressTracking, /* log message */ "MDI from "+hostnameFromWhichToArchive+" into "+hostnameOfArchive);
final CompareServersResult compareServersResult;
final String mongoDbArchivingErrorMessage;
if (mdiProgress != null && !mdiProgress.failed() && mdiProgress.getResult() != null) {
logger.info("MDI from "+hostnameFromWhichToArchive+" info "+hostnameOfArchive+" succeeded. Waiting "+durationToWaitBeforeCompareServers+" before starting to compare content...");
if (Util.isEmpty(from.getLeaderboardGroupIds())) {
logger.info("Empty set of leaderboard groups imported. Not making any comparison.");
compareServersResult = null;
} else {
Thread.sleep(durationToWaitBeforeCompareServers.asMillis());
logger.info("Comparing contents now...");
compareServersResult = Wait.wait(()->from.compareServers(Optional.empty(), archive, Optional.of(from.getLeaderboardGroupIds())),
csr->!csr.hasDiffs(), /* retryOnException */ true, Optional.of(durationToWaitBeforeCompareServers.times(maxNumberOfCompareServerAttempts)),
durationToWaitBeforeCompareServers, Level.INFO, "Comparing leaderboard groups with IDs "+Util.joinStrings(", ", from.getLeaderboardGroupIds())+
" between importing server "+hostnameOfArchive+" and exporting server "+hostnameFromWhichToArchive);
}
if (Util.isEmpty(from.getLeaderboardGroupIds()) || compareServersResult != null) {
if (Util.isEmpty(from.getLeaderboardGroupIds()) || !compareServersResult.hasDiffs()) {
logger.info("No differences found during comparing server contents. Moving on...");
final Set<UUID> eventIDs = new HashSet<>();
for (final Iterable<UUID> eids : Util.map(mdiResult.getLeaderboardGroupsImported(), lgWithEventIds->lgWithEventIds.getEventIds())) {
Util.addAll(eids, eventIDs);
}
final ReverseProxy<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>, RotatingFileBasedLog> reverseProxyCluster =
getLandscape().getCentralReverseProxy(region);
// TODO bug5311: when refactoring this for general scope migration, moving to a dedicated replica set will not require this
// TODO bug5311: when refactoring this for general scope migration, moving into a cold storage server other than ARCHIVE will require ALBToReverseProxyRedirectMapper instead
logger.info("Adding reverse proxy rules for migrated content pointing to ARCHIVE");
defaultRedirect.accept(new ALBToReverseProxyArchiveRedirectMapper<>(
reverseProxyCluster, hostnameFromWhichToArchive, Optional.ofNullable(optionalKeyName), passphraseForPrivateKeyDecryption));
if (removeApplicationReplicaSet) {
logger.info("Removing remote sailing server references to "+from+" from archive server "+archive);
try {
archive.removeRemoteServerReference(from);
} catch (Exception e) {
logger.log(Level.INFO, "Exception trying to remove remote server reference to "+from+
"; probably such a reference didn't exist");
}
logger.info("Removing the application replica set archived ("+from+") was requested");
mongoDbArchivingErrorMessage = removeApplicationReplicaSet(regionId, applicationReplicaSetToArchive, moveDatabaseHere, optionalKeyName, passphraseForPrivateKeyDecryption);
} else {
mongoDbArchivingErrorMessage = null;
logger.info("Removing remote sailing server references to events on "+from+" with IDs "+eventIDs+" from archive server "+archive);
archive.removeRemoteServerEventReferences(from, eventIDs);
}
} else {
mongoDbArchivingErrorMessage = null;
logger.severe("Even after "+maxNumberOfCompareServerAttempts+" attempts and waiting a total of "+
durationToWaitBeforeCompareServers.times(maxNumberOfCompareServerAttempts)+
" there were the following differences between exporting server "+hostnameFromWhichToArchive+
" and importing server "+hostnameOfArchive+":\nDifferences on importing side: "+compareServersResult.getADiffs()+
"\nDifferences on exporting side: "+compareServersResult.getBDiffs()+
"\nNot proceeding further. You need to resolve the issues manually.");
}
} else {
mongoDbArchivingErrorMessage = null;
logger.severe("Even after "+maxNumberOfCompareServerAttempts+" attempts and waiting a total of "+
durationToWaitBeforeCompareServers.times(maxNumberOfCompareServerAttempts)+
" the comparison of servers "+hostnameOfArchive+" and "+hostnameFromWhichToArchive+
" did not produce a result. Not proceeding. You have to resolve the issue manually.");
}
} else {
logger.severe("The Master Data Import (MDI) from "+hostnameFromWhichToArchive+" into "+hostnameOfArchive+
" did not work"+(mdiProgress != null ? mdiProgress.getErrorMessage() : " (no result at all)"));
compareServersResult = null;
mongoDbArchivingErrorMessage = null;
}
sendMailToReplicaSetOwner(archiveReplicaSet, "FinishedToArchiveReplicaSetIntoSubject", "FinishedToArchiveReplicaSetIntoBody", Optional.of(ServerActions.CAN_IMPORT_MASTERDATA));
sendMailToReplicaSetOwner(applicationReplicaSetToArchive, "FinishedToArchiveReplicaSetSubject", "FinishedToArchiveReplicaSetBody", Optional.of(ServerActions.CAN_EXPORT_MASTERDATA));
return new Util.Triple<>(mdiProgress, compareServersResult, mongoDbArchivingErrorMessage);
}
private void terminateReplicasNotManagedByAutoScalingGroup(AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> applicationReplicaSet,
String optionalKeyName, byte[] passphraseForPrivateKeyDecryption) throws InterruptedException, ExecutionException {
// only terminate replica if not running on host created by auto-scaling group
final AwsAutoScalingGroup autoScalingGroup = applicationReplicaSet.getAutoScalingGroup();
for (final SailingAnalyticsProcess<String> replica : applicationReplicaSet.getReplicas()) {
if (autoScalingGroup == null || !replica.getHost().isManagedByAutoScalingGroup(Collections.singleton(autoScalingGroup))) {
logger.info("Found replica "+replica+" running on an instance not managed by auto-scaling group " +
(autoScalingGroup != null ? autoScalingGroup.getName() : "") + ". Stopping...");
replica.stopAndTerminateIfLast(Landscape.WAIT_FOR_PROCESS_TIMEOUT, Optional.ofNullable(optionalKeyName), passphraseForPrivateKeyDecryption);
}
}
}
@Override
public String removeApplicationReplicaSet(String regionId,
AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> applicationReplicaSet,
MongoEndpoint moveDatabaseHere, String optionalKeyName, byte[] passphraseForPrivateKeyDecryption)
throws Exception {
if (applicationReplicaSet.isLocalReplicaSet()) {
throw new IllegalArgumentException("A replica set cannot remove itself. Current replica set: "+ServerInfo.getName());
}
final AwsRegion region = new AwsRegion(regionId, getLandscape());
final AwsAutoScalingGroup autoScalingGroup = applicationReplicaSet.getAutoScalingGroup();
final CompletableFuture<Void> autoScalingGroupRemoval;
// Remove all shards
for (AwsShard<String> shard : applicationReplicaSet.getShards().keySet()) {
applicationReplicaSet.removeShard(shard, getLandscape());
}
// Before stopping all replicas, make sure the master is contained in the public target group.
// Sometimes, operators may manually remove it to keep the master from receiving too much pressure.
// In this case, stopping all replicas would make the replica set become unavailable for public read access:
logger.info("Adding the master "+applicationReplicaSet.getMaster().getHost().getId()+" to public target group "+
applicationReplicaSet.getMasterTargetGroup().getName()+" if it isn't already contained");
getLandscape().addTargetsToTargetGroup(applicationReplicaSet.getPublicTargetGroup(), Collections.singleton(applicationReplicaSet.getMaster().getHost()));
// Only then can we start terminating the replica processes:
terminateReplicasNotManagedByAutoScalingGroup(applicationReplicaSet, optionalKeyName, passphraseForPrivateKeyDecryption);
if (autoScalingGroup != null) {
// remove the launch configuration used by the auto scaling group and the auto scaling group itself;
// this will also terminate all replicas spun up by the auto-scaling group
autoScalingGroupRemoval = getLandscape().removeAutoScalingGroupAndLaunchConfiguration(autoScalingGroup);
Wait.wait(()->isAllAutoScalingReplicasShutDown(applicationReplicaSet, autoScalingGroup),
Landscape.WAIT_FOR_HOST_TIMEOUT, Duration.ONE_SECOND.times(10),
Level.INFO, "Waiting for auto-scaling replicas to shut down");
} else {
autoScalingGroupRemoval = new CompletableFuture<>();
autoScalingGroupRemoval.complete(null);
}
// important to obtain this before shutting down master because then master won't reply to the MONGODB_URI request anymore
final Database fromDatabase = applicationReplicaSet.getMaster().getDatabaseConfiguration(region,
Landscape.WAIT_FOR_PROCESS_TIMEOUT, Optional.ofNullable(optionalKeyName),
passphraseForPrivateKeyDecryption);
autoScalingGroupRemoval.thenAccept(v->{
// remove the load balancer rules
try {
getLandscape().deleteLoadBalancerListenerRules(region, Util.toArray(applicationReplicaSet.getLoadBalancerRules(), new Rule[0]));
applicationReplicaSet.getMaster().stopAndTerminateIfLast(Landscape.WAIT_FOR_PROCESS_TIMEOUT, Optional.ofNullable(optionalKeyName), passphraseForPrivateKeyDecryption);
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
});
// remove the target groups
getLandscape().deleteTargetGroup(applicationReplicaSet.getMasterTargetGroup());
getLandscape().deleteTargetGroup(applicationReplicaSet.getPublicTargetGroup());
final String loadBalancerDNSName = applicationReplicaSet.getLoadBalancer().getDNSName();
final Iterable<Rule> currentLoadBalancerRuleSet = applicationReplicaSet.getLoadBalancer().getRules();
if (applicationReplicaSet.getResourceRecordSet() != null) {
// remove the load balancer if it is a DNS-mapped one and there are no rules left other than the default rule
if (applicationReplicaSet.getResourceRecordSet().resourceRecords().stream().filter(rr->
AwsLandscape.removeTrailingDotFromHostname(rr.value()).equals(loadBalancerDNSName)).findAny().isPresent() &&
(Util.isEmpty(currentLoadBalancerRuleSet) ||
(Util.size(currentLoadBalancerRuleSet) == 1 && currentLoadBalancerRuleSet.iterator().next().isDefault()))) {
logger.info("No more rules "+(!Util.isEmpty(currentLoadBalancerRuleSet) ? "except default rule " : "")+
"left in load balancer "+applicationReplicaSet.getLoadBalancer().getName()+" which was DNS-mapped; deleting.");
applicationReplicaSet.getLoadBalancer().delete();
} else {
logger.info("Keeping load balancer "+loadBalancerDNSName+" because it is not DNS-mapped or still has rules.");
}
// remove the DNS record if this replica set was a DNS-mapped one
logger.info("Removing DNS CNAME record "+applicationReplicaSet.getResourceRecordSet());
getLandscape().removeDNSRecord(applicationReplicaSet.getHostedZoneId(), applicationReplicaSet.getHostname(), RRType.CNAME, loadBalancerDNSName);
} else {
logger.info("Keeping load balancer "+loadBalancerDNSName+" because it is not DNS-mapped.");
}
final String mongoDBArchivingErrorMessage;
if (moveDatabaseHere != null) {
if (moveDatabaseHere.equals(fromDatabase.getEndpoint())) {
mongoDBArchivingErrorMessage = "Requested to move database "+fromDatabase+" to its own endpoint "+moveDatabaseHere+
". Not executing because this would kill the database.";
logger.warning(mongoDBArchivingErrorMessage);
} else {
final Database toDatabase = moveDatabaseHere.getDatabase(fromDatabase.getName());
logger.info("Archiving the database content of "+fromDatabase.getConnectionURI()+" to "+toDatabase.getConnectionURI());
final CopyAndCompareMongoDatabase<String> copyAndCompareMongoDatabaseProcedure = getCopyAndCompareMongoDatabaseProcedure(fromDatabase, toDatabase);
copyAndCompareMongoDatabaseProcedure.run();
mongoDBArchivingErrorMessage = copyAndCompareMongoDatabaseProcedure.getMongoDbArchivingErrorMessage();
}
} else {
mongoDBArchivingErrorMessage = null;
logger.info("No archiving of database content was requested. Leaving "+fromDatabase.getConnectionURI()+" untouched.");
}
getSecurityService().deleteAllDataForRemovedObject(SecuredSecurityTypes.SERVER.getQualifiedObjectIdentifier(
new TypeRelativeObjectIdentifier(applicationReplicaSet.getServerName())));
return mongoDBArchivingErrorMessage;
}
private boolean isAllAutoScalingReplicasShutDown(
AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> applicationReplicaSet,
AwsAutoScalingGroup autoScalingGroup) throws Exception {
final Iterable<SailingAnalyticsProcess<String>> replicas = applicationReplicaSet.getMaster().getReplicas(Landscape.WAIT_FOR_PROCESS_TIMEOUT,
new SailingAnalyticsHostSupplier<String>(), processFactoryFromHostAndServerDirectory);
for (final SailingAnalyticsProcess<String> replica : replicas) {
if (replica.getHost().isManagedByAutoScalingGroup(Collections.singleton(autoScalingGroup)) && replica.isAlive(Landscape.WAIT_FOR_PROCESS_TIMEOUT)) {
logger.info("Replica "+replica+" is managed by auto-scaling group "+autoScalingGroup.getName()+" and is still alive.");
return false;
}
}
return true;
}
/**
* Returns a procedure that drops the {@link #toDatabase} first; then copies {@code fromDatabase} to {@code toDatabase} and starts comparing
* the result with the original. If the comparison fails, the {@link CopyAndCompareMongoDatabase#run()} method will throw an
* {@link IllegalStateException}. If the comparison succeeds, the {@code fromDatabase} will then be deleted.
*/
private <BuilderT extends CopyAndCompareMongoDatabase.Builder<BuilderT, String>> CopyAndCompareMongoDatabase<String>
getCopyAndCompareMongoDatabaseProcedure(Database fromDatabase, Database toDatabase) throws Exception {
BuilderT builder = CopyAndCompareMongoDatabase.<BuilderT, String>builder()
.dropTargetFirst(true)
.dropSourceAfterSuccessfulCopy(true)
.setSourceDatabase(fromDatabase)
.setTargetDatabase(toDatabase)
.setAdditionalDatabasesToDelete(Collections.singleton(fromDatabase.getWithDifferentName(
fromDatabase.getName()+SailingAnalyticsReplicaConfiguration.Builder.DEFAULT_REPLICA_DATABASE_NAME_SUFFIX)));
builder
.setLandscape(getLandscape());
return builder.build();
}
private DataImportProgress waitForMDICompletionOrError(SailingServer archive,
UUID idForProgressTracking, String logMessage) throws Exception {
return Wait.wait(()->archive.getMasterDataImportProgress(idForProgressTracking), progress->progress.failed() || progress.getResult() != null,
/* retryOnException */ false, LandscapeService.MDI_TIMEOUT, LandscapeService.TIME_TO_WAIT_BETWEEN_MDI_COMPLETION_CHECKS,
Level.INFO, logMessage);
}
private SecurityService getSecurityService() {
try {
return securityServiceTracker.getInitializedService(0);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
@Override
public String getDefaultRedirectPath(Rule defaultRedirectRule) {
final String result;
if (defaultRedirectRule == null) {
result = null;
} else {
result = defaultRedirectRule.actions().stream().map(action->RedirectDTO.toString(action.redirectConfig().path(),
Optional.ofNullable(action.redirectConfig().query()))).findAny().orElse(null);
}
return result;
}
@Override
public AwsLandscape<String> getLandscape() {
final String keyId;
final String secret;
final String sessionToken;
final AwsSessionCredentialsWithExpiry sessionCredentials = getSessionCredentials();
final AwsLandscape<String> result;
if (sessionCredentials != null) {
keyId = sessionCredentials.getAccessKeyId();
secret = sessionCredentials.getSecretAccessKey();
sessionToken = sessionCredentials.getSessionToken();
result = AwsLandscape.obtain(keyId, secret, sessionToken, RemoteServiceMappingConstants.pathPrefixForShardingKey);
} else {
result = null;
}
return result;
}
/**
* For the logged-in user checks the LANDSCAPE:MANAGE:AWS permission, and if present, tries to obtain the user preference
* named like {@link #USER_PREFERENCE_FOR_SESSION_TOKEN}. If found and not yet expired, they are returned. Otherwise,
* {@code null} is returned, indicating to the caller that new session credentials shall be obtained which shall then be
* stored to the user preference again for future reference.
*/
@Override
public AwsSessionCredentialsWithExpiry getSessionCredentials() {
final AwsSessionCredentialsWithExpiry result;
final AwsSessionCredentialsFromUserPreference credentialsPreferences = getSecurityService().getPreferenceObject(
getSecurityService().getCurrentUser().getName(), USER_PREFERENCE_FOR_SESSION_TOKEN);
if (credentialsPreferences != null) {
final AwsSessionCredentialsWithExpiry credentials = credentialsPreferences.getAwsSessionCredentialsWithExpiry();
if (credentials.getExpiration().before(TimePoint.now())) {
result = null;
} else {
result = credentials;
}
} else {
result = null;
}
return result;
}
@Override
public void createMfaSessionCredentials(String awsAccessKey, String awsSecret, String mfaTokenCode) {
final Credentials credentials = AwsLandscape.obtain(awsAccessKey, awsSecret, RemoteServiceMappingConstants.pathPrefixForShardingKey).getMfaSessionCredentials(mfaTokenCode);
final AwsSessionCredentialsWithExpiryImpl result = new AwsSessionCredentialsWithExpiryImpl(
credentials.accessKeyId(), credentials.secretAccessKey(), credentials.sessionToken(),
TimePoint.of(credentials.expiration().toEpochMilli()));
final AwsSessionCredentialsFromUserPreference credentialsPreferences = new AwsSessionCredentialsFromUserPreference(result);
getSecurityService().setPreferenceObject(
getSecurityService().getCurrentUser().getName(), LandscapeService.USER_PREFERENCE_FOR_SESSION_TOKEN, credentialsPreferences);
}
@Override
public void createSessionCredentials(String awsKeyId, String awsKeySecret, String sessionToken) {
final AwsSessionCredentialsWithExpiryImpl result = new AwsSessionCredentialsWithExpiryImpl(
awsKeyId, awsKeySecret, sessionToken,
// for the expiry, guess that the creation of the session token passed here didn't happen more than five minutes ago
TimePoint.now().plus(Duration.ONE_HOUR.times(12).minus(Duration.ONE_MINUTE.times(5))));
final AwsSessionCredentialsFromUserPreference credentialsPreferences = new AwsSessionCredentialsFromUserPreference(result);
getSecurityService().setPreferenceObject(
getSecurityService().getCurrentUser().getName(), LandscapeService.USER_PREFERENCE_FOR_SESSION_TOKEN, credentialsPreferences);
}
@Override
public boolean hasValidSessionCredentials() {
return getSessionCredentials() != null;
}
@Override
public void clearSessionCredentials() {
getSecurityService().unsetPreference(getSecurityService().getCurrentUser().getName(), LandscapeService.USER_PREFERENCE_FOR_SESSION_TOKEN);
}
@Override
public String getFullyQualifiedHostname(String unqualifiedHostname, Optional<String> optionalDomainName) {
final String domainName = optionalDomainName.orElse(SharedLandscapeConstants.DEFAULT_DOMAIN_NAME);
final String fullyQualifiedHostname = unqualifiedHostname+"."+domainName;
return fullyQualifiedHostname;
}
@Override
public Release getRelease(String releaseNameOrNullForLatestMaster) {
return releaseNameOrNullForLatestMaster==null
? SailingReleaseRepository.INSTANCE.getLatestMasterRelease()
: SailingReleaseRepository.INSTANCE.getRelease(releaseNameOrNullForLatestMaster);
}
private void establishServerGroupAndTryToMakeCurrentUserItsOwnerAndMember(String serverName,
String bearerTokenUsedByReplicas, String securityServiceHostname,
Integer securityServicePort)
throws MalformedURLException, ClientProtocolException, IOException, ParseException, IllegalAccessException {
final String serverGroupName = serverName + ServerInfo.SERVER_GROUP_NAME_SUFFIX;
final SailingServerFactory sailingServerFactory = sailingServerFactoryTracker.getService();
final SailingServer securityServiceServer = sailingServerFactory.getSailingServer(
RemoteServerUtil.getBaseServerUrl(securityServiceHostname, securityServicePort==null?443:securityServicePort), bearerTokenUsedByReplicas);
final UUID userGroupId = securityServiceServer.getUserGroupIdByName(serverGroupName);
final UUID groupId;
if (userGroupId != null) {
groupId = userGroupId;
final TypeRelativeObjectIdentifier serverGroupTypeRelativeObjectId = new TypeRelativeObjectIdentifier(userGroupId.toString());
final Iterable<Pair<WildcardPermission, Boolean>> permissions = securityServiceServer.hasPermissions(Arrays.asList(
SecuredSecurityTypes.USER_GROUP.getPermissionForTypeRelativeIdentifier(DefaultActions.CREATE, serverGroupTypeRelativeObjectId),
SecuredSecurityTypes.USER_GROUP.getPermissionForTypeRelativeIdentifier(DefaultActions.READ, serverGroupTypeRelativeObjectId),
SecuredSecurityTypes.USER_GROUP.getPermissionForTypeRelativeIdentifier(DefaultActions.UPDATE, serverGroupTypeRelativeObjectId),
SecuredSecurityTypes.USER_GROUP.getPermissionForTypeRelativeIdentifier(DefaultActions.DELETE, serverGroupTypeRelativeObjectId),
SecuredSecurityTypes.SERVER.getPermissionForTypeRelativeIdentifier(DefaultActions.CREATE, serverGroupTypeRelativeObjectId),
SecuredSecurityTypes.SERVER.getPermissionForTypeRelativeIdentifier(DefaultActions.READ, serverGroupTypeRelativeObjectId),
SecuredSecurityTypes.SERVER.getPermissionForTypeRelativeIdentifier(DefaultActions.UPDATE, serverGroupTypeRelativeObjectId),
SecuredSecurityTypes.SERVER.getPermissionForTypeRelativeIdentifier(DefaultActions.DELETE, serverGroupTypeRelativeObjectId)));
for (final Pair<WildcardPermission, Boolean> permission : permissions) {
if (!permission.getB()) {
final String msg = "Subject "+securityServiceServer.getUsername()+" on server "+securityServiceHostname+
" is not allowed "+permission.getA()+". Not allowing to create application replica set for "+serverName;
logger.warning(msg);
throw new AuthorizationException(msg);
}
}
// Now we know the user is permitted to create/read/update/delete the user group and the server object in the remote
// security realm that the application replica set's master process will use if it existed already. Add the user to the group
securityServiceServer.addCurrentUserToGroup(userGroupId);
} else {
groupId = securityServiceServer.createUserGroupAndAddCurrentUser(serverGroupName);
try {
securityServiceServer.addRoleToUser(ServerAdminRole.getInstance().getId(), securityServiceServer.getUsername(),
/* qualified for server group: */ groupId, null, /* transitive */ true);
} catch (Exception e) {
// this didn't work, but it's not the end of the world if we cannot grant the requesting user the
// event_manager:{group-name} role; the user may end up not having SERVER:CREATE_OBJECT...
logger.warning("Couldn't grant role "+ServerAdminRole.getInstance().getName()+" to user "+securityServiceServer.getUsername()+": "+e.getMessage());
}
try {
// try to set the group owner of the new group to the group itself, allowing all users with role user:{group-name} to
// change / edit it.
securityServiceServer.setGroupAndUserOwner(SecuredSecurityTypes.USER_GROUP, new TypeRelativeObjectIdentifier(groupId.toString()),
Optional.empty() /* displayName */, Optional.of(groupId), Optional.empty() /* leave user owner unchanged */);
} catch (Exception e) {
// this didn't work, but it's not the end of the world if we cannot update the new group's ownership,
// although it's a bit surprising because the user identified by the bearerToken should be the group's
// owner...
logger.warning("Couldn't update user group ownership of user group "+serverGroupName+": "+e.getMessage());
}
}
ensureGroupMembersCanReadGroup(securityServiceServer, groupId);
}
private void ensureGroupMembersCanReadGroup(SailingServer securityServiceServer, UUID groupId) throws ClientProtocolException, IOException, ParseException {
// cleanly copy result as it may be unmodifiable
final Map<UUID, Set<String>> acls = new HashMap<>(securityServiceServer.getAccessControlLists(SecuredSecurityTypes.USER_GROUP, new TypeRelativeObjectIdentifier(groupId.toString())));
final Set<String> actionsForGroup = acls.computeIfAbsent(groupId, gid->new HashSet<>());
if (!actionsForGroup.contains(DefaultActions.READ.name()) && !actionsForGroup.contains("!"+DefaultActions.READ.name())) {
actionsForGroup.add(DefaultActions.READ.name());
securityServiceServer.setAccessControlLists(SecuredSecurityTypes.USER_GROUP, new TypeRelativeObjectIdentifier(groupId.toString()), acls);
}
}
private <AppConfigBuilderT extends com.sap.sailing.landscape.procedures.SailingAnalyticsMasterConfiguration.Builder<AppConfigBuilderT, String>> AppConfigBuilderT createMasterConfigurationBuilder(
String replicaSetName, String optionalMasterReplicationBearerTokenOrNull, Integer optionalMemoryInMegabytesOrNull,
Integer optionalMemoryTotalSizeFactorOrNull, final AwsRegion region, final Release release) {
final AppConfigBuilderT masterConfigurationBuilder = SailingAnalyticsMasterConfiguration.masterBuilder();
final String bearerTokenUsedByMaster = getEffectiveBearerToken(optionalMasterReplicationBearerTokenOrNull);
final User currentUser = getSecurityService().getCurrentUser();
if (currentUser != null && currentUser.isEmailValidated() && currentUser.getEmail() != null) {
masterConfigurationBuilder.setCommaSeparatedEmailAddressesToNotifyOfStartup(currentUser.getEmail());
}
masterConfigurationBuilder
.setLandscape(getLandscape())
.setServerName(replicaSetName)
.setRelease(release)
.setRegion(region)
// TODO bug5684: probably this is the place to add the REPLICATE_MASTER_SERVLET_HOST/REPLICATE_MASTER_EXCHANGE_NAME variables to point to a default security service?
.setInboundReplicationConfiguration(InboundReplicationConfiguration.builder().setCredentials(new BearerTokenReplicationCredentials(bearerTokenUsedByMaster)).build());
applyMemoryConfigurationToApplicationConfigurationBuilder(masterConfigurationBuilder, optionalMemoryInMegabytesOrNull, optionalMemoryTotalSizeFactorOrNull);
return masterConfigurationBuilder;
}
/**
* No specific memory configuration is made here; replicas are mostly launched on a dedicated host and hence can
* grab as much memory as they can get on that host. Callers that want to deploy to a shared host should use
* {@link Builder#setMemoryInMegabytes(int)} and/or {@link Builder#setMemoryTotalSizeFactor(int)} to be more
* specific about the memory configuration.
*/
private <AppConfigBuilderT extends SailingAnalyticsReplicaConfiguration.Builder<AppConfigBuilderT, String>>
AppConfigBuilderT createReplicaConfigurationBuilder(final AwsRegion region,
String replicaSetName, final int masterPort, final Release release,
final String bearerTokenUsedByReplicas, final String masterHostname) {
final AppConfigBuilderT replicaConfigurationBuilder = SailingAnalyticsReplicaConfiguration.replicaBuilder();
final User currentUser = getSecurityService().getCurrentUser();
if (currentUser != null && currentUser.isEmailValidated() && currentUser.getEmail() != null) {
replicaConfigurationBuilder.setCommaSeparatedEmailAddressesToNotifyOfStartup(currentUser.getEmail());
}
// no specific memory configuration is made here; replicas are mostly launched on a dedicated host and hence can
// grab as much memory as they can get on that host
replicaConfigurationBuilder
.setLandscape(getLandscape())
.setRegion(region)
.setServerName(replicaSetName)
.setRelease(release)
.setPort(masterPort) // replicas need to run on the same port for target group "interoperability"
.setInboundReplicationConfiguration(InboundReplicationConfiguration.builder()
.setMasterHostname(masterHostname) // don't set the master port; the replica set talks to "itself" through the load balancer using HTTPS
.setCredentials(new BearerTokenReplicationCredentials(bearerTokenUsedByReplicas))
.build());
return replicaConfigurationBuilder;
}
private void applyMemoryConfigurationToApplicationConfigurationBuilder(
final AwsApplicationConfiguration.Builder<?, ?, ?, ?, ?> applicationConfigurationBuilder,
Integer optionalMemoryInMegabytesOrNull, Integer optionalMemoryTotalSizeFactorOrNull) {
if (optionalMemoryInMegabytesOrNull != null) {
applicationConfigurationBuilder.setMemoryInMegabytes(optionalMemoryInMegabytesOrNull);
} else if (optionalMemoryTotalSizeFactorOrNull != null) {
applicationConfigurationBuilder.setMemoryTotalSizeFactor(optionalMemoryTotalSizeFactorOrNull);
}
}
private AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> createLoadBalancingAndAutoScalingSetup(
final AwsLandscape<String> landscape, final AwsRegion region, String replicaSetName,
final SailingAnalyticsProcess<String> master, final Release release, String replicaInstanceType,
boolean dynamicLoadBalancerMapping, String optionalKeyName, byte[] privateKeyEncryptionPassphrase,
String optionalDomainName,
final Optional<AmazonMachineImage<String>> replicaMachineImage,
final String bearerTokenUsedByReplicas, Optional<Integer> minimumNumberOfReplicas, Optional<Integer> maximumNumberOfReplicas)
throws Exception, JSchException, IOException, InterruptedException, SftpException, TimeoutException {
logger.info("Creating load balancing and auto-scaling set-up for replica set "+replicaSetName);
if (dynamicLoadBalancerMapping && !region.getId().equals(SharedLandscapeConstants.REGION_WITH_DEFAULT_LOAD_BALANCER)) {
// see bug5669:
throw new IllegalArgumentException("You must not request dynamic load balancer mapping in regions other than "+
SharedLandscapeConstants.REGION_WITH_DEFAULT_LOAD_BALANCER+
"; you tried it for region "+region.getId());
}
final CreateLoadBalancerMapping.Builder<?, ?, String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> createLoadBalancerMappingBuilder =
dynamicLoadBalancerMapping ? CreateDynamicLoadBalancerMapping.builder() : CreateDNSBasedLoadBalancerMapping.builder();
final String masterHostname = getHostname(replicaSetName, optionalDomainName);
final CreateLoadBalancerMapping<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> createLoadBalancerMapping = createLoadBalancerMappingBuilder
.setProcess(master)
.setHostname(masterHostname)
.setTargetGroupNamePrefix(SAILING_TARGET_GROUP_NAME_PREFIX)
.setSecurityGroupForVpc(landscape.getDefaultSecurityGroupForApplicationHosts(region))
.setLandscape(landscape)
.build();
createLoadBalancerMapping.run();
// construct a replica configuration which is used to produce the user data for the launch configuration used in an auto-scaling group
final Builder<?, String> replicaConfigurationBuilder = createReplicaConfigurationBuilder(region, replicaSetName, master.getPort(), release, bearerTokenUsedByReplicas, masterHostname);
// Now wait for master to become healthy before creating auto-scaling; otherwise it may happen that the replica tried to start
// replication before the master is ready (see also bug 5527).
master.waitUntilReady(Landscape.WAIT_FOR_HOST_TIMEOUT);
final CreateLaunchConfigurationAndAutoScalingGroup.Builder<String, ?, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> createLaunchConfigurationAndAutoScalingGroupBuilder =
CreateLaunchConfigurationAndAutoScalingGroup.builder(landscape, region, replicaSetName, createLoadBalancerMapping.getPublicTargetGroup());
createLaunchConfigurationAndAutoScalingGroupBuilder
.setInstanceType(InstanceType.valueOf(replicaInstanceType))
.setTags(Tags.with(StartAwsHost.NAME_TAG_NAME, StartSailingAnalyticsHost.INSTANCE_NAME_DEFAULT_PREFIX+replicaSetName+" (Auto-Replica)")
.and(SharedLandscapeConstants.SAILING_ANALYTICS_APPLICATION_HOST_TAG, replicaSetName))
.setOptionalTimeout(Landscape.WAIT_FOR_HOST_TIMEOUT)
.setReplicaConfiguration(replicaConfigurationBuilder.build()); // use the default scaling parameters (currently 1/30/30000)
minimumNumberOfReplicas.ifPresent(minNumberOfReplicas->createLaunchConfigurationAndAutoScalingGroupBuilder.setMinReplicas(minNumberOfReplicas));
maximumNumberOfReplicas.ifPresent(maxNumberOfReplicas->createLaunchConfigurationAndAutoScalingGroupBuilder.setMaxReplicas(maxNumberOfReplicas));
if (replicaMachineImage.isPresent()) {
createLaunchConfigurationAndAutoScalingGroupBuilder.setImage(replicaMachineImage.get());
} else {
// obtain the latest AMI for launching a Sailing Analytics replica host:
createLaunchConfigurationAndAutoScalingGroupBuilder.setImage(
StartSailingAnalyticsReplicaHost.replicaHostBuilder(replicaConfigurationBuilder)
.setLandscape(getLandscape())
.setRegion(region)
.getMachineImage());
}
if (optionalKeyName != null) {
createLaunchConfigurationAndAutoScalingGroupBuilder.setKeyName(optionalKeyName);
}
createLaunchConfigurationAndAutoScalingGroupBuilder.build().run();
final CompletableFuture<Iterable<ApplicationLoadBalancer<String>>> allLoadBalancersInRegion = landscape.getLoadBalancersAsync(region);
final CompletableFuture<Map<TargetGroup<String>, Iterable<TargetHealthDescription>>> allTargetGroupsInRegion = landscape.getTargetGroupsAsync(region);
final CompletableFuture<Map<Listener, Iterable<Rule>>> allLoadBalancerRulesInRegion = landscape.getLoadBalancerListenerRulesAsync(region, allLoadBalancersInRegion);
final CompletableFuture<Iterable<AutoScalingGroup>> autoScalingGroups = landscape.getAutoScalingGroupsAsync(region);
final CompletableFuture<Iterable<LaunchConfiguration>> launchConfigurations = landscape.getLaunchConfigurationsAsync(region);
final DNSCache dnsCache = landscape.getNewDNSCache();
final AwsApplicationReplicaSet<String,SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> applicationReplicaSet =
new AwsApplicationReplicaSetImpl<>(replicaSetName, masterHostname, master, /* no replicas yet */ Optional.empty(),
allLoadBalancersInRegion, allTargetGroupsInRegion, allLoadBalancerRulesInRegion,
autoScalingGroups, launchConfigurations, dnsCache, RemoteServiceMappingConstants.pathPrefixForShardingKey);
return applicationReplicaSet;
}
@Override
public String getHostname(String replicaSetName, String optionalDomainName) {
final String domainName = Optional.ofNullable(optionalDomainName).orElse(SharedLandscapeConstants.DEFAULT_DOMAIN_NAME);
final String masterHostname = replicaSetName+"."+domainName;
return masterHostname;
}
@Override
public AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> upgradeApplicationReplicaSet(AwsRegion region,
AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet,
String releaseOrNullForLatestMaster, String optionalKeyName, byte[] privateKeyEncryptionPassphrase,
String replicaReplicationBearerToken)
throws MalformedURLException, IOException, TimeoutException, Exception {
if (replicaSet.isLocalReplicaSet()) {
throw new IllegalArgumentException(
"A replica set cannot upgrade itself. Current replica set: " + ServerInfo.getName());
}
if (!replicaSet.getShards().isEmpty() && replicaSet.getAutoScalingGroup() == null) {
throw new IllegalStateException(
"A replica set is expected to have an auto scaling group if it has shards");
}
final Release release = getRelease(releaseOrNullForLatestMaster);
final String effectiveReplicaReplicationBearerToken = getEffectiveBearerToken(replicaReplicationBearerToken);
final int oldAutoScalingGroupMinSize;
final AwsAutoScalingGroup autoScalingGroup = replicaSet.getAutoScalingGroup();
final Collection<AwsAutoScalingGroup> affectedAutoScalingGroups = new ArrayList<>();
final int autoScalingReplicaCount;
replicaSet.getShards().keySet().forEach(t -> affectedAutoScalingGroups.add(t.getAutoScalingGroup()));
if (autoScalingGroup != null) {
affectedAutoScalingGroups.add(autoScalingGroup);
oldAutoScalingGroupMinSize = autoScalingGroup.getAutoScalingGroup().minSize();
int tempAutoScalingReplicaCount = 0;
for (AwsAutoScalingGroup asg : affectedAutoScalingGroups) {
tempAutoScalingReplicaCount = tempAutoScalingReplicaCount + asg.getAutoScalingGroup().desiredCapacity();
}
autoScalingReplicaCount = tempAutoScalingReplicaCount;
} else {
oldAutoScalingGroupMinSize = -1;
autoScalingReplicaCount = -1;
}
final Set<SailingAnalyticsProcess<String>> replicasToStopAfterUpgradingMaster = new HashSet<>();
Util.addAll(replicaSet.getReplicas(), replicasToStopAfterUpgradingMaster);
final SailingAnalyticsProcess<String> additionalReplicaStarted = ensureAtLeastOneReplicaExistsStopReplicatingAndRemoveMasterFromTargetGroups(
replicaSet, optionalKeyName, privateKeyEncryptionPassphrase, effectiveReplicaReplicationBearerToken);
if (replicaSet.getAutoScalingGroup() != null) {
getLandscape().updateReleaseInAutoScalingGroups(region, replicaSet.getAutoScalingGroup().getLaunchConfiguration(),
affectedAutoScalingGroups, replicaSet.getName(), release);
}
final SailingAnalyticsProcess<String> master = replicaSet.getMaster();
master.refreshToRelease(release, Optional.ofNullable(optionalKeyName), privateKeyEncryptionPassphrase);
// wait for master to turn healthy:
logger.info("Waiting for master "+master+" to get ready with new release "+release.getName());
master.waitUntilReady(Optional.of(Duration.ONE_DAY)); // wait a little longer since master may need to re-load many races
logger.info("Launching upgrade replicas based on current set of replicas for replica set "+replicaSet.getName());
final Iterable<SailingAnalyticsProcess<String>> temporaryUpgradeReplicas = launchUpgradeReplicasAndWaitUntilReady(
replicaSet, release, effectiveReplicaReplicationBearerToken, Optional.ofNullable(optionalKeyName),
privateKeyEncryptionPassphrase);
final List<SailingAnalyticsProcess<String>> temporaryUpgradeReplicasMutable = Util.asList(temporaryUpgradeReplicas);
// register master again with master and public target group
logger.info("Adding master " + master + " and dedicated upgraded temporary replicas to target groups "
+ replicaSet.getPublicTargetGroup() + " and " + replicaSet.getMasterTargetGroup());
replicaSet.getPublicTargetGroup().addTarget(master.getHost());
replicaSet.getMasterTargetGroup().addTarget(master.getHost());
sendMailAboutMasterAvailable(replicaSet);
// the following map stores the assignment of the temporary upgrade replicas to the public / shard target groups
// for later removal as the final auto-scaling replicas start to replace them:
final Map<TargetGroup<String>, Iterable<AwsInstance<String>>> tempUpgradeReplicasByTargetGroup = new HashMap<>();
// add as many temporary upgrade replicas to each shard's target group as the target group has targets now:
for (final AwsShard<String> shard : replicaSet.getShards().keySet()) {
final TargetGroup<String> shardTargetGroup = shard.getTargetGroup();
final Collection<AwsInstance<String>> hosts = new ArrayList<>();
final int numberOfTargets = shardTargetGroup.getRegisteredTargets().size();
for (int i = 0; i < numberOfTargets; i++) {
final SailingAnalyticsProcess<String> tempUpgradeReplicaProcess = temporaryUpgradeReplicasMutable.remove(0);
shardTargetGroup.addTarget(tempUpgradeReplicaProcess.getHost());
hosts.add(tempUpgradeReplicaProcess.getHost());
}
tempUpgradeReplicasByTargetGroup.put(shardTargetGroup, hosts);
}
// add all other temporary upgrade replicas not used for shards to the public target group:
replicaSet.getPublicTargetGroup().addTargets(Util.map(temporaryUpgradeReplicasMutable,
temporaryUpgradeReplica -> temporaryUpgradeReplica.getHost()));
tempUpgradeReplicasByTargetGroup.put(replicaSet.getPublicTargetGroup(), Util.map(temporaryUpgradeReplicasMutable, temporaryUpgradeReplica->temporaryUpgradeReplica.getHost()));
// if a replica was spun up (additionalReplicaStarted), remove from public target group and terminate:
if (additionalReplicaStarted != null) {
replicasToStopAfterUpgradingMaster.add(additionalReplicaStarted);
if (replicaSet.getAutoScalingGroup() != null) {
// run updating the auto-scaling group in the background; it's more time-critical now
// to remove the old replicas from the target group
for (AwsAutoScalingGroup asg : affectedAutoScalingGroups) {
ThreadPoolUtil.INSTANCE.getDefaultBackgroundTaskThreadPoolExecutor()
.execute(ThreadPoolUtil.INSTANCE.associateWithSubjectIfAny(() -> {
getLandscape().updateAutoScalingGroupMinSize(asg, oldAutoScalingGroupMinSize);
}));
}
} // else, the replica was started explicitly, without an auto-scaling group; in any case, all replicas still
// on the old release will now be stopped:
}
final Set<SailingAnalyticsProcess<String>> newUpgradedUnmanagedReplicas = new HashSet<>();
logger.info("Removing old replicas from public target group "+replicaSet.getPublicTargetGroup());
replicaSet.getPublicTargetGroup().removeTargets(Util.map(replicasToStopAfterUpgradingMaster, replica->replica.getHost()));
for (final SailingAnalyticsProcess<String> replica : replicasToStopAfterUpgradingMaster) {
// if managed by auto-scaling group or if it's an "unmanaged" / "explicit" replica and it's the one launched in order to have at least one, stop/terminate;
final boolean managedByAutoScalingGroup = replica.getHost().isManagedByAutoScalingGroup(affectedAutoScalingGroups);
if (managedByAutoScalingGroup ||
(additionalReplicaStarted != null && replica.getHost().getInstanceId().equals(additionalReplicaStarted.getHost().getInstanceId()))) {
logger.info("Stopping (and terminating if last application process on host) replicas on old release: "+replicasToStopAfterUpgradingMaster);
replica.stopAndTerminateIfLast(Landscape.WAIT_FOR_PROCESS_TIMEOUT, Optional.ofNullable(optionalKeyName), privateKeyEncryptionPassphrase);
} else { // otherwise, upgrade in place and add to target group again when ready
logger.info("Refreshing unmanaged replica "+replica+" in place");
replica.refreshToRelease(release, Optional.ofNullable(optionalKeyName), privateKeyEncryptionPassphrase);
replica.waitUntilReady(Optional.of(Duration.ONE_DAY));
replicaSet.getPublicTargetGroup().addTarget(replica.getHost());
newUpgradedUnmanagedReplicas.add(replica);
}
}
final Iterable<SailingAnalyticsProcess<String>> newUpgradedReplicas;
if (autoScalingGroup != null && autoScalingReplicaCount > 0) {
logger.info("Now waiting until " + autoScalingReplicaCount
+ " auto-scaling replicas are ready before taking down dedicated temporary upgrade replicas");
// Note: the wait call returns *all* of the master's replicas, not only the auto-scaling ones; it's the predicate that
// filters them down to the auto-scaling ones, counts them and compares them to the required count
newUpgradedReplicas = Wait.wait(() -> master.getReplicas(Landscape.WAIT_FOR_PROCESS_TIMEOUT,
new SailingAnalyticsHostSupplier<String>(), new SailingAnalyticsProcessFactory(this::getLandscape)),
replicas -> {
int size = Util.size(Util.filter(replicas,
replica -> replica.getHost().isManagedByAutoScalingGroup(affectedAutoScalingGroups)));
logger.info("Until now there are " + size + " auto-scaling replicas healthy");
return size >= autoScalingReplicaCount;
}, /* retryOnException */ true, Optional.of(Duration.ONE_DAY),
/* duration between attempts */ Duration.ONE_SECOND.times(30), Level.INFO,
"Waiting for " + autoScalingReplicaCount + " auto-scaling replicas to become ready");
} else {
logger.info("No auto-scaling group or auto-scaling group did not have managed instances; using only the upgraded unmanaged replicas "+
newUpgradedUnmanagedReplicas);
newUpgradedReplicas = newUpgradedUnmanagedReplicas;
}
logger.info("Removing targets for all temporary upgrade replicas " + temporaryUpgradeReplicas);
for (Entry<TargetGroup<String>, Iterable<AwsInstance<String>>> entry : tempUpgradeReplicasByTargetGroup.entrySet()) {
entry.getKey().removeTargets(entry.getValue());
}
logger.info("Stopping/terminating all temporary upgrade replicas "+temporaryUpgradeReplicas);
for (final SailingAnalyticsProcess<String> upgradeReplica : temporaryUpgradeReplicas) {
upgradeReplica.stopAndTerminateIfLast(Landscape.WAIT_FOR_PROCESS_TIMEOUT, Optional.ofNullable(optionalKeyName), privateKeyEncryptionPassphrase);
}
return getLandscape().getApplicationReplicaSet(region, replicaSet.getServerName(), master,
// don't use those temporary upgrade replicas that just got terminated:
Util.filter(newUpgradedReplicas, newUpgradedReplica->!Util.contains(temporaryUpgradeReplicas, newUpgradedReplica)));
}
/**
* Launches as many temporary upgrade replicas as are currently available in
* {@link AwsApplicationReplicaSet#getReplicas() the replica set's replicas}, in the same availability zones as the
* existing ones, and with the same instance types as the existing ones, then waits for them to become ready. The
* replicas launched here are <em>not</em> added to any target group by this method. The differences compared to the
* original replicas will be:
* <ul>
* <li>The new replicas will use the {@code release} specified which is expected to be the new release to which the
* master already has been switched.</li>
* <li>The new replicas will be named and tagged such that they can be recognized by an operator as dedicated
* upgrade replicas</li>
* <li>Instead of using the replica set's {@link AwsApplicationReplicaSet#getHostname() hostname} to identify the
* master process to replicate from, the replicas produced by this method use the master's IP address and port
* so that replication works also while the master is not registered with the replica set's master target group.</li>
* </ul>
* <p>
*
* The replicas will be launched with their master configuration not based on the hostname but on the master's IP
* address, therefore not depending on the master being registered in the "-m" target group. With this, old replicas
* on the old version can remain in place with no other version showing up in either target group until all upgrade
* replicas have become ready and can then be used to populate the public target group, together with the master
* which is additionally registered in the "-m" target group then. This "switching" process happens in probably less
* than a second and should reduce the time span during which processes running different versions may be
* encountered to a minimum.
*
* @return the replica processes launched
*/
private <AppConfigBuilderT extends SailingAnalyticsReplicaConfiguration.Builder<AppConfigBuilderT, String>>
Iterable<SailingAnalyticsProcess<String>> launchUpgradeReplicasAndWaitUntilReady(
AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet,
Release release, String replicationBearerToken, Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception {
final Set<SailingAnalyticsProcess<String>> result = new HashSet<>();
final SailingAnalyticsProcess<String> master = replicaSet.getMaster();
final AwsRegion region = master.getHost().getRegion();
for (final SailingAnalyticsProcess<String> replica : replicaSet.getReplicas()) {
final InstanceType instanceType = replica.getHost().getInstance().instanceType();
// Determine the replica node's current heap size configuration ("MEMORY" variable) and
// use it to set the upgrade replica's explicit memory size:
final String memoryVariable = replica.getEnvShValueFor(DefaultProcessConfigurationVariables.MEMORY, Landscape.WAIT_FOR_PROCESS_TIMEOUT,
optionalKeyName, privateKeyEncryptionPassphrase);
final Optional<Integer> megabytesFromJvmSize = JvmUtils.getMegabytesFromJvmSize(memoryVariable);
logger.info("Determined replica set "+replicaSet.getName()+"'s replica memory size for replica "+replica+
" as "+memoryVariable+" which equals "+megabytesFromJvmSize+
"MB. Using for upgrade replica configuration with instance type "+instanceType+".");
final AppConfigBuilderT replicaConfigurationBuilder =
createReplicaConfigurationBuilder(region, replicaSet.getServerName(), master.getPort(),
release, replicationBearerToken, replicaSet.getHostname());
// regarding the dedicated temporary upgrade replica's memory configuration we can assume that either the
// old replica was running on a dedicated instance and therefore had a memory configuration that uses the
// instance's available RAM, so will fit into the new dedicated temporary upgrade instance; or the
// old replica was on a shared instance; in this case we'll over-provision, but it won't be long.
replicaConfigurationBuilder
.setInboundReplicationConfiguration(InboundReplicationConfiguration.builder()
.setMasterHostname(master.getHost().getPrivateAddress().getHostAddress())
.setMasterHttpPort(master.getPort())
.setCredentials(new BearerTokenReplicationCredentials(replicationBearerToken))
.build());
megabytesFromJvmSize.ifPresent(
memoryInMegabytes->replicaConfigurationBuilder.setMemoryInMegabytes(memoryInMegabytes));
final com.sap.sailing.landscape.procedures.StartSailingAnalyticsReplicaHost.Builder<?, String> replicaHostBuilder =
StartSailingAnalyticsReplicaHost.replicaHostBuilder(replicaConfigurationBuilder);
replicaHostBuilder
.setInstanceName(StartSailingAnalyticsHost.INSTANCE_NAME_DEFAULT_PREFIX+replicaSet.getName()+TEMPORARY_UPGRADE_REPLICA_NAME_SUFFIX)
.setInstanceType(instanceType)
.setOptionalTimeout(Landscape.WAIT_FOR_HOST_TIMEOUT)
.setLandscape(getLandscape())
.setRegion(region)
.setTags(Tags.with(UPGRADE_REPLICA_TAG_KEY, replicaSet.getName()));
optionalKeyName.ifPresent(replicaHostBuilder::setKeyName);
final StartSailingAnalyticsReplicaHost<String> replicaHostStartProcedure = replicaHostBuilder.build();
logger.info("Launching dedicated replica host of type "+instanceType+" for replica "+replica);
replicaHostStartProcedure.run();
Wait.wait(()->replicaHostStartProcedure.getSailingAnalyticsProcess().getHost().getInstance(), instance->instance != null, /* retryOnException */ true,
Landscape.WAIT_FOR_HOST_TIMEOUT, /* sleepBetweenAttempts */ Duration.ONE_SECOND.times(10), Level.WARNING,
"Waiting for replica instance with ID "+replicaHostStartProcedure.getHost().getInstanceId());
result.add(replicaHostStartProcedure.getSailingAnalyticsProcess());
}
for (final SailingAnalyticsProcess<String> resultReplica : result) {
logger.info("Waiting for replica "+resultReplica+" to become ready");
resultReplica.waitUntilReady(Optional.of(Duration.ONE_DAY)); // the my or archive server will take that long...
}
return result;
}
@Override
public Iterable<SailingAnalyticsHost<String>> getEligibleSharedHostsForReplicaSet(AwsRegion region,
final AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet,
String optionalKeyName, byte[] privateKeyEncryptionPassphrase) {
return Util.filter(getLandscape().getRunningHostsWithTagValue(region,
SharedLandscapeConstants.SAILING_ANALYTICS_APPLICATION_HOST_TAG, SharedLandscapeConstants.MULTI_PROCESS_INSTANCE_TAG_VALUE,
new SailingAnalyticsHostSupplier<String>()), h->{
try {
return replicaSet.isEligibleForDeployment(h, Landscape.WAIT_FOR_PROCESS_TIMEOUT, Optional.ofNullable(optionalKeyName), privateKeyEncryptionPassphrase);
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
@Override
public AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> getApplicationReplicaSet(
final AwsRegion region, String replicaSetName, Long optionalTimeoutInMilliseconds, String optionalKeyName,
byte[] passphraseForPrivateKeyDecryption) throws Exception {
return Util.first(Util.filter(
getLandscape().getApplicationReplicaSetsByTag(region,
SharedLandscapeConstants.SAILING_ANALYTICS_APPLICATION_HOST_TAG, new SailingAnalyticsHostSupplier<String>(),
Optional.ofNullable(optionalTimeoutInMilliseconds).map(Duration::ofMillis), Optional.ofNullable(optionalKeyName),
passphraseForPrivateKeyDecryption),
rs->rs.getName().equals(replicaSetName)));
}
/**
* @return a new replica that was started in case no running replica was found in the {@code replicaSet}, otherwise
* {@code null}.
*/
@Override
public SailingAnalyticsProcess<String> ensureAtLeastOneReplicaExistsStopReplicatingAndRemoveMasterFromTargetGroups(
final AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet,
String optionalKeyName, byte[] privateKeyEncryptionPassphrase,
final String effectiveReplicaReplicationBearerToken) throws Exception, MalformedURLException, IOException,
TimeoutException, InterruptedException, ExecutionException {
final Set<SailingAnalyticsProcess<String>> replicasToStopReplicating = new HashSet<>();
Util.addAll(replicaSet.getReplicas(), replicasToStopReplicating);
final SailingAnalyticsProcess<String> additionalReplicaStarted;
if (Util.isEmpty(Util.filter(new HashSet<>(replicasToStopReplicating), replica ->
// exclude replicas belonging to a shard
!replica.getHost().isManagedByAutoScalingGroup(replicaSet.getShards().keySet().stream().map(
shard->shard.getAutoScalingGroup())::iterator)))) {
logger.info("No replica that doesn't belong to any shard was found for replica set " + replicaSet.getName()
+ "; spinning one up and waiting for it to become healthy");
additionalReplicaStarted = launchReplicaAndWaitUntilHealthy(replicaSet,
Optional.ofNullable(optionalKeyName), privateKeyEncryptionPassphrase,
effectiveReplicaReplicationBearerToken);
replicasToStopReplicating.add(additionalReplicaStarted);
} else {
additionalReplicaStarted = null;
}
logger.info("Stopping replication for replica set "+replicaSet.getName());
for (final SailingAnalyticsProcess<String> replica : replicasToStopReplicating) {
logger.info("...asking replica "+replica+" to stop replication");
try {
replica.stopReplicatingFromMaster(effectiveReplicaReplicationBearerToken, Landscape.WAIT_FOR_PROCESS_TIMEOUT);
} catch (Exception e) {
logger.log(Level.SEVERE, "Telling replica "+replica+" to stop replicating from master didn't work; assuming it's dead; continuing...", e);
}
}
logger.info("Done stopping replication. Removing master "+replicaSet.getMaster()+" from target groups "+
replicaSet.getPublicTargetGroup()+" and "+replicaSet.getMasterTargetGroup());
replicaSet.getPublicTargetGroup().removeTarget(replicaSet.getMaster().getHost());
replicaSet.getMasterTargetGroup().removeTarget(replicaSet.getMaster().getHost());
sendMailAboutMasterUnavailable(replicaSet);
return additionalReplicaStarted;
}
/**
* For the {@code replicaSet}, find out how a replica can be spun up.
* <ul>
* <li>If there is an
* {@link AwsApplicationReplicaSet#getAutoScalingGroup() auto-scaling group} in place, ensure that
* its {@link AutoScalingGroup#minSize() minimum size} is at least one, then wait for a replica
* to show up and become healthy.</li>
* <li>Without an auto-scaling group, configure and run a {@link StartSailingAnalyticsReplicaHost} procedure
* and wait for its {@link StartSailingAnalyticsReplicaHost#getHost()} to become healthy, then
* {@link TargetGroup#addTarget(AwsInstance) add} the replica to the public target group.</li>
* </ul>
*
* @return the replica launched
*/
private SailingAnalyticsProcess<String> launchReplicaAndWaitUntilHealthy(
AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet,
Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase, String replicationBearerToken)
throws Exception {
final SailingAnalyticsProcess<String> spunUpReplica;
if (replicaSet.getAutoScalingGroup() != null) {
spunUpReplica = spinUpReplicaByIncreasingAutoScalingGroupMinSize(replicaSet.getAutoScalingGroup(), replicaSet.getMaster());
} else {
spunUpReplica = spinUpReplicaAndRegisterInPublicTargetGroup(replicaSet, optionalKeyName,
privateKeyEncryptionPassphrase, replicationBearerToken);
}
return spunUpReplica;
}
private SailingAnalyticsProcess<String> spinUpReplicaAndRegisterInPublicTargetGroup(
AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet,
Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase, String replicationBearerToken) throws Exception {
final AwsRegion region = replicaSet.getMaster().getHost().getRegion();
return spinUpReplicaAndRegisterInPublicTargetGroup(region, replicaSet, optionalKeyName, privateKeyEncryptionPassphrase, replicationBearerToken,
/* processLauncher: */ replicaConfigurationBuilder->{
// the process launcher determines the master's instance type and launches a host of the same type using the replica configuration for the process config
final InstanceType masterInstanceType = replicaSet.getMaster().getHost().getInstance().instanceType();
final com.sap.sailing.landscape.procedures.StartSailingAnalyticsReplicaHost.Builder<?, String> replicaHostBuilder = StartSailingAnalyticsReplicaHost.replicaHostBuilder(replicaConfigurationBuilder);
replicaHostBuilder
.setInstanceType(masterInstanceType)
.setOptionalTimeout(Landscape.WAIT_FOR_HOST_TIMEOUT)
.setLandscape(getLandscape())
.setRegion(region)
.setPrivateKeyEncryptionPassphrase(privateKeyEncryptionPassphrase);
optionalKeyName.ifPresent(keyName->replicaHostBuilder.setKeyName(keyName));
try {
final StartSailingAnalyticsReplicaHost<String> replicaHostStartProcedure = replicaHostBuilder.build();
replicaHostStartProcedure.run();
return replicaHostStartProcedure.getSailingAnalyticsProcess();
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
/**
* Launches a replica process for a given replica set and adds it to the replica set's public target group. The way
* the replica process is launched is provided as a {@link Function} in the ({@code processLauncher} parameter). The
* function may, e.g., use the replica process configuration to launch a new host which automatically deploys a
* replica process according to this configuration, or the function may for example use the replica process
* configuration to parameterize a process deployment to an existing host (see the
* {@link DeployProcessOnMultiServer} procedure).
*
* @return the process that the {@code processLauncher} started
*/
private <AppConfigBuilderT extends SailingAnalyticsReplicaConfiguration.Builder<AppConfigBuilderT, String>>
SailingAnalyticsProcess<String> spinUpReplicaAndRegisterInPublicTargetGroup(
AwsRegion region,
AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet,
Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase, String replicationBearerToken,
Function<AppConfigBuilderT, SailingAnalyticsProcess<String>> processLauncher) throws Exception {
final Release release = replicaSet.getVersion(Landscape.WAIT_FOR_PROCESS_TIMEOUT, optionalKeyName, privateKeyEncryptionPassphrase);
final AppConfigBuilderT replicaConfigurationBuilder =
createReplicaConfigurationBuilder(region, replicaSet.getServerName(), replicaSet.getMaster().getPort(), release, replicationBearerToken, replicaSet.getHostname());
final SailingAnalyticsProcess<String> sailingAnalyticsProcess = processLauncher.apply(replicaConfigurationBuilder);
waitUntilHealthyAndThenRegisterReplicaInPublicTargetGroup(sailingAnalyticsProcess, replicaSet);
return sailingAnalyticsProcess;
}
/**
* Waits for the process to become ready (see {@link SailingAnalyticsProcess#waitUntilReady(Optional)}),
* then registers its {@link SailingAnalyticsProcess#getHost() host} with the {@code replicaSet}'s
* {@link AwsApplicationReplicaSet#getPublicTargetGroup() public target group}.
*/
private void waitUntilHealthyAndThenRegisterReplicaInPublicTargetGroup(
final SailingAnalyticsProcess<String> sailingAnalyticsProcess,
final AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet) throws TimeoutException, Exception {
sailingAnalyticsProcess.waitUntilReady(Landscape.WAIT_FOR_HOST_TIMEOUT);
if (replicaSet.getPublicTargetGroup() != null) {
replicaSet.getPublicTargetGroup().addTarget(sailingAnalyticsProcess.getHost());
}
}
private SailingAnalyticsProcess<String> spinUpReplicaByIncreasingAutoScalingGroupMinSize(
AwsAutoScalingGroup autoScalingGroup,
SailingAnalyticsProcess<String> master)
throws TimeoutException, Exception {
if (autoScalingGroup.getAutoScalingGroup().minSize() < 1) {
getLandscape().updateAutoScalingGroupMinSize(autoScalingGroup, 1);
}
return Wait.wait(()->hasHealthyAutoScalingReplica(master, autoScalingGroup), healthyReplica->healthyReplica != null,
/* retryOnException */ true,
Landscape.WAIT_FOR_HOST_TIMEOUT, Duration.ONE_SECOND.times(5), Level.INFO,
"Waiting for auto-scaling group to produce healthy replica");
}
/**
* Returns one replica process managed by {@code autoScalingGroup} that is healthy, or {@code null} if no such process was found
*/
private SailingAnalyticsProcess<String> hasHealthyAutoScalingReplica(SailingAnalyticsProcess<String> master, AwsAutoScalingGroup autoScalingGroup) throws Exception {
final HostSupplier<String, SailingAnalyticsHost<String>> hostSupplier = new SailingAnalyticsHostSupplier<>();
for (final SailingAnalyticsProcess<String> replica : master.getReplicas(Landscape.WAIT_FOR_HOST_TIMEOUT, hostSupplier, processFactoryFromHostAndServerDirectory)) {
if (replica.getHost().isManagedByAutoScalingGroup(Collections.singleton(autoScalingGroup)) && replica.isReady(Landscape.WAIT_FOR_HOST_TIMEOUT)) {
return replica;
}
}
return null;
}
@Override
public <BuilderT extends StartMultiServer.Builder<BuilderT, String>> SailingAnalyticsHost<String> createEmptyMultiServer(AwsRegion region, Optional<InstanceType> instanceType,
Optional<AwsAvailabilityZone> availabilityZone, Optional<String> name, Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception {
final StartMultiServer.Builder<BuilderT, String> startMultiServerProcedureBuilder = StartMultiServer.<BuilderT, String>builder();
startMultiServerProcedureBuilder
.setLandscape(getLandscape())
.setRegion(region);
instanceType.ifPresent(it->startMultiServerProcedureBuilder.setInstanceType(it));
availabilityZone.ifPresent(az->startMultiServerProcedureBuilder.setAvailabilityZone(az));
optionalKeyName.ifPresent(keyName->startMultiServerProcedureBuilder.setKeyName(keyName));
if (privateKeyEncryptionPassphrase != null) {
startMultiServerProcedureBuilder.setPrivateKeyEncryptionPassphrase(privateKeyEncryptionPassphrase);
}
name.ifPresent(nameTag->startMultiServerProcedureBuilder.setInstanceName(nameTag));
final StartMultiServer<String> startMultiServerProcedure = startMultiServerProcedureBuilder.build();
startMultiServerProcedure.run();
return Wait.wait(() -> startMultiServerProcedure.getHost(), multiServer -> {
try {
return multiServer.isReady(optionalKeyName, privateKeyEncryptionPassphrase);
} catch (Exception e) {
return false;
}
}, true, Landscape.WAIT_FOR_HOST_TIMEOUT, /* sleepBetweenAttempts */ Duration.ONE_SECOND.times(10), Level.INFO, "Waiting until host "+startMultiServerProcedure.getHost().getId()+" is ready");
}
@Override
public Iterable<AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>>> updateImageForReplicaSets(AwsRegion region,
Iterable<AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>>> replicaSets,
Optional<AmazonMachineImage<String>> optionalAmi) throws InterruptedException, ExecutionException, TimeoutException {
final Set<AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>>> result = new HashSet<>();
for (final AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet : replicaSets) {
if (replicaSet.getAutoScalingGroup() != null) {
final AmazonMachineImage<String> ami = optionalAmi.orElseGet(
()->getLandscape().getLatestImageWithType(region, SharedLandscapeConstants.IMAGE_TYPE_TAG_VALUE_SAILING));
logger.info("Upgrading AMI in auto-scaling groups "+Util.join(", ", replicaSet.getAllAutoScalingGroups())+" of replica set "+replicaSet.getName()+" to "+ami.getId());
getLandscape().updateImageInAutoScalingGroups(region, replicaSet.getAllAutoScalingGroups(), replicaSet.getName(), ami);
result.add(getLandscape().getApplicationReplicaSet(region, replicaSet.getServerName(), replicaSet.getMaster(), replicaSet.getReplicas()));
} else {
logger.info("No auto-scaling group found for replica set "+replicaSet.getName()+" to update AMI in");
}
}
return result;
}
@Override
public <AppConfigBuilderT extends Builder<AppConfigBuilderT, String>,
MultiServerDeployerBuilderT extends com.sap.sailing.landscape.procedures.DeployProcessOnMultiServer.Builder<MultiServerDeployerBuilderT, String, SailingAnalyticsHost<String>, SailingAnalyticsReplicaConfiguration<String>, AppConfigBuilderT>>
AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> useDedicatedAutoScalingReplicasInsteadOfShared(
AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet,
String optionalKeyName, byte[] privateKeyEncryptionPassphrase)
throws Exception {
final AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> result;
if (replicaSet.getAutoScalingGroup() != null) {
final Integer minSize = replicaSet.getAutoScalingGroup().getAutoScalingGroup().minSize();
if (minSize != null && minSize > 0) {
logger.info("Replica set "+replicaSet+" already has its auto-scaling group minimum size set to a non-zero value: "+minSize);
result = replicaSet;
} else {
final SailingAnalyticsProcess<String> replica = spinUpReplicaByIncreasingAutoScalingGroupMinSize(replicaSet.getAutoScalingGroup(), replicaSet.getMaster());
assert replica.isReady(Landscape.WAIT_FOR_PROCESS_TIMEOUT);
for (final SailingAnalyticsProcess<String> nonAutoScalingReplica : replicaSet.getReplicas()) {
if (!nonAutoScalingReplica.getHost().isManagedByAutoScalingGroup()) {
logger.info("Found replica "+nonAutoScalingReplica+" to be not managed by auto-scaling group "+replicaSet.getAutoScalingGroup().getName()+
". Removing it from Target Group and stopping it...");
replicaSet.getPublicTargetGroup().removeTarget(nonAutoScalingReplica.getHost());
nonAutoScalingReplica.stopAndTerminateIfLast(Landscape.WAIT_FOR_PROCESS_TIMEOUT, Optional.ofNullable(optionalKeyName), privateKeyEncryptionPassphrase);
}
}
result = getLandscape().getApplicationReplicaSet(replicaSet.getMaster().getHost().getRegion(), replicaSet.getServerName(), replicaSet.getMaster(),
Collections.singleton(replica));
}
} else {
logger.warning("No auto-scaling group found for replica set "+replicaSet+"; not terminating any replicas.");
result = null;
}
return result;
}
@Override
public <AppConfigBuilderT extends Builder<AppConfigBuilderT, String>,
MultiServerDeployerBuilderT extends DeployProcessOnMultiServer.Builder<MultiServerDeployerBuilderT, String, SailingAnalyticsHost<String>, SailingAnalyticsReplicaConfiguration<String>, AppConfigBuilderT>>
AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> useSingleSharedInsteadOfDedicatedAutoScalingReplica(
AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet,
String optionalKeyName, byte[] privateKeyEncryptionPassphrase,
String replicaReplicationBearerToken, Integer optionalMemoryInMegabytesOrNull,
Integer optionalMemoryTotalSizeFactorOrNull, Optional<InstanceType> optionalInstanceType) throws Exception {
final AwsAutoScalingGroup autoScalingGroup = replicaSet.getAutoScalingGroup();
final Set<SailingAnalyticsProcess<String>> nonAutoScalingReplica = new HashSet<>();
for (final SailingAnalyticsProcess<String> replica : replicaSet.getReplicas()) {
if (autoScalingGroup == null || !replica.getHost().isManagedByAutoScalingGroup(Collections.singleton(autoScalingGroup))) {
logger.info("Found replica "+replica+" in replica set "+replicaSet.getName()+
" which is not managed by auto-scaling group");
nonAutoScalingReplica.add(replica);
}
}
if (nonAutoScalingReplica.isEmpty()) {
logger.info("No replica found for replica set "+replicaSet.getName()+
" that is not managed by auto-scaling group "+(autoScalingGroup==null?"null":autoScalingGroup.getName())+
". Launching one on an eligible shared instance.");
nonAutoScalingReplica.add(launchUnmanagedReplica(replicaSet, replicaSet.getMaster().getHost().getRegion(), optionalKeyName,
privateKeyEncryptionPassphrase, getEffectiveBearerToken(replicaReplicationBearerToken), optionalMemoryInMegabytesOrNull,
optionalMemoryTotalSizeFactorOrNull,
Optional.of(optionalInstanceType.orElseGet(()->replicaSet.getMaster().getHost().getInstance().instanceType())),
/* optionalPreferredInstanceToDeployTo */ Optional.empty()));
}
if (autoScalingGroup != null) {
logger.info("Scaling down auto-scaling group for replica set "+replicaSet.getName()+" from minimum size "+
replicaSet.getAutoScalingGroup().getAutoScalingGroup().minSize()+" to 0");
getLandscape().updateAutoScalingGroupMinSize(replicaSet.getAutoScalingGroup(), 0);
} else {
logger.info("No auto-scaling group found for replica set "+replicaSet.getName()+"; nothing to scale down.");
}
return getLandscape().getApplicationReplicaSet(replicaSet.getMaster().getHost().getRegion(), replicaSet.getServerName(), replicaSet.getMaster(), nonAutoScalingReplica);
}
@Override
public <AppConfigBuilderT extends SailingAnalyticsMasterConfiguration.Builder<AppConfigBuilderT, String>,
MultiServerDeployerBuilderT extends DeployProcessOnMultiServer.Builder<MultiServerDeployerBuilderT, String, SailingAnalyticsHost<String>, SailingAnalyticsMasterConfiguration<String>, AppConfigBuilderT>>
AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> moveMasterToOtherInstance(
final AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet,
final boolean useSharedInstance, Optional<InstanceType> optionalInstanceType,
Optional<SailingAnalyticsHost<String>> optionalPreferredInstanceToDeployTo, String optionalKeyName,
byte[] privateKeyEncryptionPassphrase, String optionalMasterReplicationBearerTokenOrNull, final String optionalReplicaReplicationBearerTokenOrNull, Integer optionalMemoryInMegabytesOrNull,
Integer optionalMemoryTotalSizeFactorOrNull)
throws MalformedURLException,
IOException, TimeoutException, InterruptedException, ExecutionException, Exception {
if (replicaSet.isLocalReplicaSet()) {
throw new IllegalArgumentException("A replica set cannot move its own master process. Current replica set: "+ServerInfo.getName());
}
final SailingAnalyticsProcess<String> newTemporaryReplica = ensureAtLeastOneReplicaExistsStopReplicatingAndRemoveMasterFromTargetGroups(replicaSet,
optionalKeyName, privateKeyEncryptionPassphrase,
getEffectiveBearerToken(optionalReplicaReplicationBearerTokenOrNull));
// important to obtain the release before stopping master:
final Release release = replicaSet.getVersion(Landscape.WAIT_FOR_PROCESS_TIMEOUT, Optional.ofNullable(optionalKeyName), privateKeyEncryptionPassphrase);
final AwsRegion region = replicaSet.getMaster().getHost().getRegion();
SailingAnalyticsHost<String> hostToDeployTo = null;
if (useSharedInstance) {
// determine new shared host before stopping old master; we want to *move* and not end up on the same instance again
hostToDeployTo = new EligibleInstanceForReplicaSetFindingStrategyImpl(this,
region, optionalKeyName, privateKeyEncryptionPassphrase,
/* master */ true, /* mustBeDifferentAvailabilityZone */ true, optionalInstanceType,
optionalPreferredInstanceToDeployTo).getInstanceToDeployTo(replicaSet);
}
logger.info("Stopping master "+replicaSet.getMaster());
replicaSet.getMaster().stopAndTerminateIfLast(Landscape.WAIT_FOR_PROCESS_TIMEOUT, Optional.ofNullable(optionalKeyName), privateKeyEncryptionPassphrase);
final AppConfigBuilderT masterConfigurationBuilder = createMasterConfigurationBuilder(replicaSet.getName(),
optionalMasterReplicationBearerTokenOrNull, optionalMemoryInMegabytesOrNull, optionalMemoryTotalSizeFactorOrNull,
region, release);
masterConfigurationBuilder.setPort(replicaSet.getPort()); // master must run on same port as the rest of the replica set
final SailingAnalyticsProcess<String> newMaster;
if (useSharedInstance) {
assert hostToDeployTo != null;
logger.info("Launching new master on shared instance "+hostToDeployTo);
newMaster = deployProcessToSharedInstance(hostToDeployTo, masterConfigurationBuilder, optionalKeyName, privateKeyEncryptionPassphrase);
} else {
assert hostToDeployTo == null;
final com.sap.sailing.landscape.procedures.StartSailingAnalyticsMasterHost.Builder<?, String> masterHostBuilder = StartSailingAnalyticsMasterHost.masterHostBuilder(masterConfigurationBuilder);
masterHostBuilder
.setInstanceType(optionalInstanceType.orElse(InstanceType.valueOf(SharedLandscapeConstants.DEFAULT_DEDICATED_INSTANCE_TYPE_NAME)))
.setOptionalTimeout(Landscape.WAIT_FOR_HOST_TIMEOUT)
.setLandscape(getLandscape())
.setRegion(region)
.setPrivateKeyEncryptionPassphrase(privateKeyEncryptionPassphrase);
if (optionalKeyName != null) {
masterHostBuilder.setKeyName(optionalKeyName);
}
final StartSailingAnalyticsMasterHost<String> masterHostStartProcedure = masterHostBuilder.build();
masterHostStartProcedure.run();
hostToDeployTo = masterHostStartProcedure.getHost();
logger.info("Launched dedicated instance for master: "+hostToDeployTo);
newMaster = masterHostStartProcedure.getSailingAnalyticsProcess();
}
newMaster.waitUntilReady(Landscape.WAIT_FOR_HOST_TIMEOUT);
logger.info("Adding new master "+newMaster+" to target groups");
replicaSet.getPublicTargetGroup().addTarget(hostToDeployTo);
replicaSet.getMasterTargetGroup().addTarget(hostToDeployTo);
sendMailAboutMasterAvailable(replicaSet);
if (newTemporaryReplica != null) {
newTemporaryReplica.stopAndTerminateIfLast(Landscape.WAIT_FOR_HOST_TIMEOUT, Optional.ofNullable(optionalKeyName), privateKeyEncryptionPassphrase);
}
// here, although we're waiting for a process to become healthy, that process may need to fetch a large
// initial load before reaching the healthy state, so we'll use the longer HOST timeout here:
replicaSet.restartAllReplicas(Landscape.WAIT_FOR_HOST_TIMEOUT, Optional.ofNullable(optionalKeyName), privateKeyEncryptionPassphrase);
return getLandscape().getApplicationReplicaSet(region, replicaSet.getServerName(), newMaster, replicaSet.getReplicas());
}
private void sendMailAboutMasterUnavailable(
AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet) throws MailException {
sendMailToReplicaSetOwner(replicaSet, "MasterUnavailableMailSubject", "MasterUnavailableMailBody", Optional.of(ServerActions.CONFIGURE_LOCAL_SERVER));
}
private void sendMailAboutMasterAvailable(
AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet) throws MailException {
sendMailToReplicaSetOwner(replicaSet, "MasterAvailableMailSubject", "MasterAvailableMailBody", Optional.of(ServerActions.CONFIGURE_LOCAL_SERVER));
}
/**
* @param subjectMessageKey
* must have a single placeholder argument representing the name of the replica set
* @param bodyMessageKey
* must have a single placeholder argument representing the name of the replica set
* @param alsoSendToAllUsersWithThisPermissionOnReplicaSet
* when not empty, all users that have permission to this {@link SecuredSecurityTypes#SERVER SERVER}
* action on the {@code replicaSet} will receive the e-mail in addition to the server owner. No user
* will receive the e-mail twice.
*/
private void sendMailToReplicaSetOwner(
AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet,
final String subjectMessageKey, final String bodyMessageKey, Optional<Action> alsoSendToAllUsersWithThisPermissionOnReplicaSet) throws MailException {
final OwnershipAnnotation serverOwnership = getSecurityService().getOwnership(getReplicaSetQualifiedObjectIdentifier(replicaSet));
final User serverOwner;
final ResourceBundleStringMessages stringMessages = new ResourceBundleStringMessagesImpl(STRING_MESSAGES_BASE_NAME, getClass().getClassLoader(), StandardCharsets.UTF_8.name());
final Set<User> usersToSendMailTo = new HashSet<>();
if (serverOwnership != null && serverOwnership.getAnnotation() != null && (serverOwner = serverOwnership.getAnnotation().getUserOwner()) != null) {
usersToSendMailTo.add(serverOwner);
}
alsoSendToAllUsersWithThisPermissionOnReplicaSet.ifPresent(
serverAction -> getSecurityService().getUsersWithPermissions(getReplicaSetQualifiedObjectIdentifier(replicaSet).getPermission(serverAction))
.forEach(usersToSendMailTo::add));
for (final User user : usersToSendMailTo) {
final String subject = stringMessages.get(user.getLocaleOrDefault(), subjectMessageKey, replicaSet.getServerName());
final String body = stringMessages.get(user.getLocaleOrDefault(), bodyMessageKey, replicaSet.getServerName());
if (user.isEmailValidated()) {
getSecurityService().sendMail(user.getName(),
subject,
body);
} else {
logger.warning("Not sending e-mail with subject "+subject+" to user "+user.getName()+
" with e-mail address "+user.getEmail()+" because e-mail address has not been validated");
}
}
}
private QualifiedObjectIdentifier getReplicaSetQualifiedObjectIdentifier(
AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet) {
return SecuredSecurityTypes.SERVER.getQualifiedObjectIdentifier(new TypeRelativeObjectIdentifier(replicaSet.getServerName()));
}
/**
* If a non-{@code null}, non-{@link String#isEmpty() empty} bearer token is provided by the
* {@code optionalBearerTokenOnNull} parameter, it is returned unchanged; otherwise, the bearer token as obtained
* for the current session's principal is returned. See also {@link SecurityService#getOrCreateAccessToken(String)}.
*/
@Override
public String getEffectiveBearerToken(String optionalBearerTokenOrNull) {
return Util.hasLength(optionalBearerTokenOrNull) ? optionalBearerTokenOrNull :
getSecurityService().getOrCreateAccessToken(SessionUtils.getPrincipal().toString());
}
@Override
public AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> changeAutoScalingReplicasInstanceType(
final AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet,
InstanceType instanceType) throws Exception {
final AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> result;
final Iterable<AwsAutoScalingGroup> autoScalingGroups = replicaSet.getAllAutoScalingGroups();
if (!Util.isEmpty(autoScalingGroups)) {
// Don't trust the replicas passed in by the client; it may be stale. Instead, obtain a new set of replicas from the master:
final Iterable<SailingAnalyticsProcess<String>> oldReplicas = replicaSet.getMaster().getReplicas(
Landscape.WAIT_FOR_PROCESS_TIMEOUT, new SailingAnalyticsHostSupplier<String>(),
processFactoryFromHostAndServerDirectory);
Iterable<SailingAnalyticsProcess<String>> newSetOfAllReplicas = oldReplicas;
final Set<SailingAnalyticsProcess<String>> terminatedReplicas = new HashSet<>();
getLandscape().updateInstanceTypeInAutoScalingGroup(replicaSet.getMaster().getHost().getRegion(), autoScalingGroups, replicaSet.getName(), instanceType);
for (final AwsAutoScalingGroup autoScalingGroup : autoScalingGroups) {
logger.info("Rolling upgrade of instances for auto-scaling group "+autoScalingGroup.getName()+" to new instance type"+instanceType);
final int oldMinSize = autoScalingGroup.getAutoScalingGroup().minSize();
final int newMinSize = autoScalingGroup.getAutoScalingGroup().desiredCapacity() + 1;
getLandscape().updateAutoScalingGroupMinSize(autoScalingGroup, newMinSize);
for (final SailingAnalyticsProcess<String> replica : oldReplicas) {
final SailingAnalyticsHost<String> replicaHost = replica.getHost();
if (replicaHost.isManagedByAutoScalingGroup(Collections.singleton(autoScalingGroup))) {
logger.info("Replica "+replica+" is managed by auto-scaling group "+autoScalingGroup.getName());
newSetOfAllReplicas = waitUntilAtLeastSoManyAutoScalingReplicasAreReady(replicaSet, autoScalingGroup, newMinSize);
getLandscape().terminate(replicaHost);
terminatedReplicas.add(replica);
}
}
getLandscape().updateAutoScalingGroupMinSize(autoScalingGroup, oldMinSize);
}
result = getLandscape().getApplicationReplicaSet(replicaSet.getMaster().getHost().getRegion(),
replicaSet.getServerName(), replicaSet.getMaster(),
// remove terminated replicas:
Util.filter(newSetOfAllReplicas, r->!Util.contains(terminatedReplicas, r)));
} else {
logger.info("Replica set "+replicaSet.getName()+
" does not have an auto-scaling group configured, so no changes can be made to its launch configuration.");
result = replicaSet;
}
return result;
}
private Iterable<SailingAnalyticsProcess<String>> waitUntilAtLeastSoManyAutoScalingReplicasAreReady(
final AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet,
AwsAutoScalingGroup autoScalingGroup, final int newMinSize) throws Exception {
final SailingAnalyticsProcess<String> master = replicaSet.getMaster();
assert autoScalingGroup != null;
final Set<SailingAnalyticsProcess<String>> replicas = new HashSet<>();
if (Wait.wait(()->{
int readyAutoScalingReplicas = 0;
try {
replicas.clear();
Util.addAll(master.getReplicas(
Landscape.WAIT_FOR_PROCESS_TIMEOUT, new SailingAnalyticsHostSupplier<String>(),
processFactoryFromHostAndServerDirectory), replicas);
for (final SailingAnalyticsProcess<String> replica : replicas) {
if (replica.getHost().isManagedByAutoScalingGroup(Collections.singleton(autoScalingGroup))) {
if (replica.waitUntilReady(Landscape.WAIT_FOR_PROCESS_TIMEOUT)) {
readyAutoScalingReplicas++;
logger.info("Replica "+replica+" is ready; found "+readyAutoScalingReplicas+"/"+newMinSize+" so far");
if (readyAutoScalingReplicas >= newMinSize) {
break;
}
} else {
logger.info("Replica "+replica+" NOT ready; still found only "+readyAutoScalingReplicas+"/"+newMinSize+" so far");
}
}
}
} catch (TimeoutException timeoutException) {
logger.info("Timeout looking for replicas: "+timeoutException);
}
return readyAutoScalingReplicas >= newMinSize;
}, Landscape.WAIT_FOR_HOST_TIMEOUT, /* duration between attempts */ Duration.ONE_SECOND.times(30),
Level.INFO, "Waiting until at least "+newMinSize+" auto-scaling replicas for replica set "+replicaSet.getName()+" are ready")) {
return replicas;
} else {
throw new TimeoutException("Could determine set of ready auto-scaling replicas of replica set "+
replicaSet.getName()+" within timeout period "+Landscape.WAIT_FOR_HOST_TIMEOUT);
}
}
/**
* Checks whether the {@code host} is eligbile for deploying a process of an application replica set named
* as defined by {@code serverName}, listening on the application port specified by {@code port}.
*/
@Override
public <ShardingKey> boolean isEligibleForDeployment(SailingAnalyticsHost<ShardingKey> host, String serverName, int port, Optional<Duration> optionalTimeout,
String optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception {
boolean result;
if (host.isManagedByAutoScalingGroup()) {
result = false;
} else {
result = true;
final Iterable<SailingAnalyticsProcess<ShardingKey>> applicationProcesses = host.getApplicationProcesses(optionalTimeout,
Optional.ofNullable(optionalKeyName), privateKeyEncryptionPassphrase);
for (final SailingAnalyticsProcess<ShardingKey> applicationProcess : applicationProcesses) {
if (applicationProcess.getPort() == port || applicationProcess.getServerName(optionalTimeout, Optional.ofNullable(optionalKeyName), privateKeyEncryptionPassphrase).equals(serverName)) {
result = false;
break;
}
}
}
return result;
}
@Override
public SailingServer getSailingServer(String hostname, String username, String password, Optional<Integer> port)
throws MalformedURLException {
final SailingServerFactory fac = sailingServerFactoryTracker.getService();
return fac.getSailingServer(RemoteServerUtil.getBaseServerUrl(hostname,
port.isPresent() ? port.get() : /* defaults to HTTPS */ 443), username, password);
}
@Override
public SailingServer getSailingServer(String hostname, String bearerToken, Optional<Integer> port)
throws MalformedURLException {
final SailingServerFactory fac = sailingServerFactoryTracker.getService();
return fac.getSailingServer(RemoteServerUtil.getBaseServerUrl(hostname,
port.orElse(443 /* defaults to HTTPS */)), bearerToken);
}
private <BuilderT extends CreateShard.Builder<BuilderT, CreateShard<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>>, String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>>> com.sap.sse.landscape.aws.orchestration.CreateShard.Builder<BuilderT, CreateShard<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>>, String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> createShardBuilder() {
return CreateShard.<SailingAnalyticsMetrics, SailingAnalyticsProcess<String>, BuilderT, String> builder();
}
private <BuilderT extends ShardProcedure.Builder<BuilderT, AddShardingKeyToShard<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>>, String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>>> com.sap.sse.landscape.aws.orchestration.ShardProcedure.Builder<BuilderT, AddShardingKeyToShard<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>>, String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> appendShardingKeyToShardBuilder() {
return AddShardingKeyToShard
.<SailingAnalyticsMetrics, SailingAnalyticsProcess<String>, BuilderT, String> builder();
}
private <BuilderT extends ShardProcedure.Builder<BuilderT, RemoveShardingKeyFromShard<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>>, String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>>> com.sap.sse.landscape.aws.orchestration.ShardProcedure.Builder<BuilderT, RemoveShardingKeyFromShard<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>>, String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> removeShardingKeyFromShardBuilder() {
return RemoveShardingKeyFromShard
.<SailingAnalyticsMetrics, SailingAnalyticsProcess<String>, BuilderT, String> builder();
}
@Override
public void removeShardingKeysFromShard(Iterable<String> selectedleaderboards,
AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> applicationReplicaSet,
AwsRegion region, String shardName, String bearerToken)
throws Exception {
final SailingServer server = getSailingServer(applicationReplicaSet.getHostname(), bearerToken,
/* HTTPS port */ Optional.of(443));
Set<String> shardingKeys = new HashSet<>();
for (String leaderboardName : selectedleaderboards) {
shardingKeys.add(server.getLeaderboardShardingKey(leaderboardName));
}
removeShardingKeyFromShardBuilder()
.setLandscape(getLandscape())
.setRegion(region)
.setPathPrefixForShardingKey(RemoteServiceMappingConstants.pathPrefixForShardingKey)
.setShardingKeys(shardingKeys)
.setReplicaSet(applicationReplicaSet)
.setShardName(shardName)
.build()
.run();
}
@Override
public void appendShardingKeysToShard(Iterable<String> selectedLeaderboards,
AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> applicationReplicaSet,
AwsRegion region, String shardName, String bearerToken)
throws Exception {
final SailingServer server = getSailingServer(applicationReplicaSet.getHostname(), bearerToken,
/* HTTPS port */ Optional.of(443));
final Set<String> shardingkeys = new HashSet<String>();
for (String s : selectedLeaderboards) {
shardingkeys.add(server.getLeaderboardShardingKey(s));
}
appendShardingKeyToShardBuilder()
.setLandscape(getLandscape())
.setRegion(region)
.setPathPrefixForShardingKey(RemoteServiceMappingConstants.pathPrefixForShardingKey)
.setShardingKeys(shardingkeys)
.setReplicaSet(applicationReplicaSet)
.setShardName(shardName)
.build()
.run();
}
@Override
public void removeShard(
AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> applicationReplicaSet,
String shardTargetGroupArn) throws Exception {
for (Entry<AwsShard<String>, Iterable<String>> entry : applicationReplicaSet.getShards().entrySet()) {
if (shardTargetGroupArn.equals(entry.getKey().getTargetGroup().getTargetGroupArn())) {
applicationReplicaSet.removeShard(entry.getKey(), getLandscape());
}
}
}
@Override
public void addShard(Iterable<String> selectedLeaderboardNames,
AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> applicationReplicaSet,
AwsRegion region, String bearerToken, String shardName)
throws Exception {
final SailingServer server = getSailingServer(applicationReplicaSet.getHostname(), bearerToken,
Optional.of(443));
final Set<String> shardingkeys = new HashSet<String>();
for (final String s : selectedLeaderboardNames) {
shardingkeys.add(server.getLeaderboardShardingKey(s));
}
createShardBuilder()
.setLandscape(getLandscape())
.setTargetGroupNamePrefix(LandscapeService.SAILING_TARGET_GROUP_NAME_PREFIX)
.setShardingKeys(shardingkeys)
.setReplicaSet(applicationReplicaSet)
.setRegion(region)
.setPathPrefixForShardingKey(RemoteServiceMappingConstants.pathPrefixForShardingKey)
.setShardName(shardName)
.build()
.run();
}
@Override
public Triple<SailingAnalyticsHost<String>, Map<String, SailingAnalyticsProcess<String>>, Map<String, SailingAnalyticsProcess<String>>>
moveAllApplicationProcessesAwayFrom(SailingAnalyticsHost<String> host,
Optional<InstanceType> optionalInstanceTypeForNewInstance,
String optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception {
if (!host.getInstance().tags().stream().anyMatch(tag->
tag.key().equals(SharedLandscapeConstants.SAILING_ANALYTICS_APPLICATION_HOST_TAG) &&
tag.value().equals(SharedLandscapeConstants.MULTI_PROCESS_INSTANCE_TAG_VALUE))) {
throw new IllegalArgumentException("Host "+host+" is not tagged as multiserver. The host is expected to have value "+
SharedLandscapeConstants.MULTI_PROCESS_INSTANCE_TAG_VALUE+" for tag "+
SharedLandscapeConstants.SAILING_ANALYTICS_APPLICATION_HOST_TAG);
}
logger.info("Moving all application processes from shared host "+host+" to a newly started shared host.");
final AwsRegion region = host.getRegion();
final HostSupplier<String, SailingAnalyticsHost<String>> hostSupplier = new SailingAnalyticsHostSupplier<>();
final SailingAnalyticsHost<String> targetHost = createEmptyMultiServer(region,
Optional.of(optionalInstanceTypeForNewInstance.orElse(host.getInstanceType())),
Optional.of(host.getAvailabilityZone()),
Optional.of(SharedLandscapeConstants.MULTI_PROCESS_INSTANCE_DEFAULT_NAME),
Optional.ofNullable(optionalKeyName), privateKeyEncryptionPassphrase);
final Map<String, SailingAnalyticsProcess<String>> masterProcessesMoved = new HashMap<>();
final Map<String, SailingAnalyticsProcess<String>> replicaProcessesMoved = new HashMap<>();
for (final AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet : getLandscape()
.getApplicationReplicaSetsByTag(region, SharedLandscapeConstants.SAILING_ANALYTICS_APPLICATION_HOST_TAG,
hostSupplier, Landscape.WAIT_FOR_PROCESS_TIMEOUT, Optional.ofNullable(optionalKeyName),
privateKeyEncryptionPassphrase)) {
if (replicaSet.getMaster().getHost().getId().equals(host.getId())) {
// We're moving a master process:
logger.info("Found master process "+replicaSet.getMaster()+" on host "+host+" to move to "+targetHost);
masterProcessesMoved.put(replicaSet.getName(), replicaSet.getMaster());
// Try to obtain the replicas' replication bearer token from a replica; if no replica is found in the
// application replica set, use the master's replication token as a default
final String replicaReplicationBearerToken = Util.stream(replicaSet.getReplicas()).findAny()
.map(replica -> {
try {
return replica.getEnvShValueFor(
DefaultProcessConfigurationVariables.REPLICATE_MASTER_BEARER_TOKEN,
Landscape.WAIT_FOR_PROCESS_TIMEOUT, Optional.ofNullable(optionalKeyName),
privateKeyEncryptionPassphrase);
} catch (Exception e) {
logger.log(Level.SEVERE, "Error trying to obtain environment variable value for "+
DefaultProcessConfigurationVariables.REPLICATE_MASTER_BEARER_TOKEN+" from host "+replica, e);
throw new RuntimeException(e);
}
})
.orElse(replicaSet.getMaster().getEnvShValueFor(
DefaultProcessConfigurationVariables.REPLICATE_MASTER_BEARER_TOKEN,
Landscape.WAIT_FOR_PROCESS_TIMEOUT, Optional.ofNullable(optionalKeyName),
privateKeyEncryptionPassphrase));
final Integer totalMemorySizeFactor = getTotalMemorySizeFactor(optionalKeyName, privateKeyEncryptionPassphrase, replicaSet.getMaster());
moveMasterToOtherInstance(replicaSet, /* useSharedInstance */ true,
/* optionalInstanceType */ Optional.empty(), Optional.of(targetHost), optionalKeyName,
privateKeyEncryptionPassphrase,
/* optionalMasterReplicationBearerTokenOrNull */ replicaSet.getMaster().getEnvShValueFor(
DefaultProcessConfigurationVariables.REPLICATE_MASTER_BEARER_TOKEN,
Landscape.WAIT_FOR_PROCESS_TIMEOUT, Optional.ofNullable(optionalKeyName),
privateKeyEncryptionPassphrase),
replicaReplicationBearerToken,
totalMemorySizeFactor == null ? getMemoryInMegabytes(optionalKeyName, privateKeyEncryptionPassphrase, replicaSet.getMaster()) : null,
totalMemorySizeFactor);
logger.info("Done moving master of "+replicaSet.getName()+" from "+host+" to "+targetHost);
} else {
final SailingAnalyticsProcess<String> replica;
if ((replica = Util.stream(replicaSet.getReplicas()).filter(r->r.getHost().getId().equals(host.getId())).findFirst().orElse(null)) != null) {
// We're moving a replica process:
logger.info("Found replica process "+replica+" on host "+host+" to move to "+targetHost);
final String replicaReplicationBearerToken = replica.getEnvShValueFor(
DefaultProcessConfigurationVariables.REPLICATE_MASTER_BEARER_TOKEN,
Landscape.WAIT_FOR_PROCESS_TIMEOUT, Optional.ofNullable(optionalKeyName),
privateKeyEncryptionPassphrase);
replicaProcessesMoved.put(replicaSet.getName(), replica);
final Integer totalMemorySizeFactor = getTotalMemorySizeFactor(optionalKeyName, privateKeyEncryptionPassphrase, replica);
final SailingAnalyticsProcess<String> newReplica = deployReplicaToExistingHost(replicaSet, targetHost, optionalKeyName, privateKeyEncryptionPassphrase,
replicaReplicationBearerToken,
totalMemorySizeFactor == null ? getMemoryInMegabytes(optionalKeyName, privateKeyEncryptionPassphrase, replica) : null,
totalMemorySizeFactor);
if (newReplica != null && newReplica.isReady(Landscape.WAIT_FOR_PROCESS_TIMEOUT)) {
logger.info("New replica " + newReplica + " deployed successfully to " + targetHost
+ "; removing old replica " + replica
+ " from public target group of application replica set " + replicaSet.getName());
replicaSet.getPublicTargetGroup().removeTarget(replica.getHost());
logger.info("Stopping old replica "+replica);
replica.stopAndTerminateIfLast(Landscape.WAIT_FOR_PROCESS_TIMEOUT, Optional.ofNullable(optionalKeyName), privateKeyEncryptionPassphrase);
}
}
}
}
return new Triple<>(targetHost, masterProcessesMoved, replicaProcessesMoved);
}
private Integer getTotalMemorySizeFactor(String optionalKeyName, byte[] privateKeyEncryptionPassphrase,
final SailingAnalyticsProcess<String> process)
throws Exception {
final String totalMemorySizeFactorAsString = process.getEnvShValueFor(DefaultProcessConfigurationVariables.TOTAL_MEMORY_SIZE_FACTOR, Landscape.WAIT_FOR_PROCESS_TIMEOUT, Optional.ofNullable(optionalKeyName), privateKeyEncryptionPassphrase);
final Integer totalMemorySizeFactor = Util.hasLength(totalMemorySizeFactorAsString) ? Integer.valueOf(totalMemorySizeFactorAsString) : null;
return totalMemorySizeFactor;
}
private Integer getMemoryInMegabytes(String optionalKeyName, byte[] privateKeyEncryptionPassphrase, final SailingAnalyticsProcess<String> process)
throws Exception {
final String memoryInMegabytesAsString = process.getEnvShValueFor(DefaultProcessConfigurationVariables.MEMORY, Landscape.WAIT_FOR_PROCESS_TIMEOUT, Optional.ofNullable(optionalKeyName), privateKeyEncryptionPassphrase);
final Integer memoryInMegabytes = JvmUtils.getMegabytesFromJvmSize(memoryInMegabytesAsString).orElse(null);
return memoryInMegabytes;
}
}