From a43dc46bb26aaf987c48d6920e62ea8a74b1d9ff Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 13 Jan 2021 01:22:55 +0100 Subject: [PATCH] bug4811: interims commit --- .../GWT Sailing SDM.launch | 154 +++++++++--------- .../landscape/test/TestProcedures.java | 38 +++-- .../landscape/test/TestReleaseRepository.java | 1 - .../META-INF/MANIFEST.MF | 3 +- .../ui/client/LandscapeManagementPanel.java | 20 ++- .../LandscapeManagementWriteService.java | 3 + .../LandscapeManagementWriteServiceAsync.java | 10 +- .../ui/client/SshKeyManagementPanel.java | 81 +++++++++ .../ui/client/i18n/StringMessages.java | 3 + .../ui/client/i18n/StringMessages.properties | 5 +- .../client/i18n/StringMessages_de.properties | 5 +- .../LandscapeManagementWriteServiceImpl.java | 24 +++ .../landscape/ui/shared/SSHKeyPairDTO.java | 40 +++++ .../landscape/SailingAnalyticsProcess.java | 6 +- .../impl/SailingAnalyticsProcessImpl.java | 7 +- .../DeployProcessOnMultiServer.java | 24 ++- .../procedures/StartMultiServer.java | 8 +- .../procedures/StartSailingAnalyticsHost.java | 2 +- .../landscape/procedures/UpgradeAmi.java | 2 +- .../gwt/client/celltable/TableWrapper.java | 17 ++ .../sse/landscape/aws/ConnectivityTest.java | 113 ++++++++++--- .../landscape/aws/ApplicationProcessHost.java | 3 +- .../sap/sse/landscape/aws/AwsLandscape.java | 25 ++- .../sap/sse/landscape/aws/ReverseProxy.java | 20 ++- .../aws/impl/ApacheReverseProxy.java | 63 ++++--- .../aws/impl/ApacheReverseProxyCluster.java | 28 ++-- .../aws/impl/ApplicationProcessHostImpl.java | 13 +- .../landscape/aws/impl/AwsInstanceImpl.java | 48 ++++-- .../landscape/aws/impl/AwsLandscapeImpl.java | 24 +-- .../CreateLoadBalancerMapping.java | 10 +- .../aws/orchestration/StartAwsHost.java | 32 +++- .../aws/orchestration/StartMongoDBServer.java | 2 - .../src/com/sap/sse/landscape/Host.java | 26 ++- .../application/ApplicationProcess.java | 16 +- .../impl/ApplicationProcessImpl.java | 78 +++++---- .../com/sap/sse/landscape/ssh/SSHKeyPair.java | 11 +- 36 files changed, 683 insertions(+), 282 deletions(-) create mode 100755 java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/SshKeyManagementPanel.java create mode 100755 java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/shared/SSHKeyPairDTO.java diff --git a/java/com.sap.sailing.gwt.ui/GWT Sailing SDM.launch b/java/com.sap.sailing.gwt.ui/GWT Sailing SDM.launch index 63e7e8dc021..280ee214cc8 100755 --- a/java/com.sap.sailing.gwt.ui/GWT Sailing SDM.launch +++ b/java/com.sap.sailing.gwt.ui/GWT Sailing SDM.launch @@ -36,84 +36,82 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sailing.landscape.test/src/com/sap/sailing/landscape/test/TestProcedures.java b/java/com.sap.sailing.landscape.test/src/com/sap/sailing/landscape/test/TestProcedures.java index 4c2c4e2ca06..d83ef4f2db8 100755 --- a/java/com.sap.sailing.landscape.test/src/com/sap/sailing/landscape/test/TestProcedures.java +++ b/java/com.sap.sailing.landscape.test/src/com/sap/sailing/landscape/test/TestProcedures.java @@ -51,6 +51,7 @@ import com.sap.sse.landscape.aws.orchestration.StartMongoDBServer; import com.sap.sse.landscape.mongodb.MongoEndpoint; import com.sap.sse.landscape.mongodb.MongoProcess; import com.sap.sse.landscape.mongodb.MongoReplicaSet; +import com.sap.sse.landscape.ssh.SSHKeyPair; import com.sap.sse.landscape.ssh.SshCommandChannel; import software.amazon.awssdk.regions.Region; @@ -78,8 +79,15 @@ public class TestProcedures { private String securityServiceReplicationBearerToken; private String mailSmtpPassword; + /** + * Used for the symmetric encryption / decryption of private SSH keys. See also + * {@link #getDecryptedPrivateKey(SSHKeyPair, byte[])}. + */ + private byte[] privateKeyEncryptionPassphrase; + @Before public void setUp() { + privateKeyEncryptionPassphrase = ("awptyf87l"+"097384sf;,57").getBytes(); landscape = AwsLandscape.obtain(); region = new AwsRegion(Region.EU_WEST_2); securityServiceReplicationBearerToken = System.getProperty(SECURITY_SERVICE_REPLICATION_BEARER_TOKEN); @@ -112,12 +120,13 @@ public class TestProcedures { @Test public void testStartupEmptyMultiServerAndDeployAnotherProcess() throws Exception { final String keyName = "MyKey-"+UUID.randomUUID(); - landscape.createKeyPair(region, keyName); + landscape.createKeyPair(region, keyName, privateKeyEncryptionPassphrase); final StartMultiServer.Builder builder = StartMultiServer.builder(); final String sailingAnalyticsServerTag = "sailing-analytics-server"; final StartMultiServer startEmptyMultiServer = builder .setLandscape(landscape) .setKeyName(keyName) + .setPrivateKeyEncryptionPassphrase(privateKeyEncryptionPassphrase) .setTags(Tags.with(sailingAnalyticsServerTag, "")) .setOptionalTimeout(optionalTimeout) .build(); @@ -125,7 +134,7 @@ public class TestProcedures { // this is expected to have connected to the default "live" replica set. startEmptyMultiServer.run(); final ApplicationProcessHost> host = startEmptyMultiServer.getHost(); - final SshCommandChannel sshChannel = host.createRootSshChannel(optionalTimeout); + final SshCommandChannel sshChannel = host.createRootSshChannel(optionalTimeout, privateKeyEncryptionPassphrase); final String result = sshChannel.runCommandAndReturnStdoutAndLogStderr("ls "+ApplicationProcessHost.DEFAULT_SERVERS_PATH, /* stderr prefix */ null, /* stderr log level */ null); assertTrue(result.isEmpty()); final HttpURLConnection connection = (HttpURLConnection) new URL("http", host.getPublicAddress().getCanonicalHostName(), 80, "").openConnection(); @@ -140,10 +149,10 @@ public class TestProcedures { assertEquals(new HashSet<>(Arrays.asList(SailingAnalyticsApplicationConfiguration.Builder.DEFAULT_PORT, SailingAnalyticsApplicationConfiguration.Builder.DEFAULT_PORT+1)), new HashSet<>(Arrays.asList(processA.getPort(), processB.getPort()))); assertEquals(new HashSet<>(Arrays.asList(SailingAnalyticsApplicationConfiguration.Builder.DEFAULT_TELNET_PORT, SailingAnalyticsApplicationConfiguration.Builder.DEFAULT_TELNET_PORT+1)), - new HashSet<>(Arrays.asList(processA.getTelnetPortToOSGiConsole(optionalTimeout), processB.getTelnetPortToOSGiConsole(optionalTimeout)))); + new HashSet<>(Arrays.asList(processA.getTelnetPortToOSGiConsole(optionalTimeout, privateKeyEncryptionPassphrase), processB.getTelnetPortToOSGiConsole(optionalTimeout, privateKeyEncryptionPassphrase)))); assertEquals(new HashSet<>(Arrays.asList(SailingAnalyticsApplicationConfiguration.Builder.DEFAULT_EXPEDITION_PORT, SailingAnalyticsApplicationConfiguration.Builder.DEFAULT_EXPEDITION_PORT+1)), - new HashSet<>(Arrays.asList(processA.getExpeditionUdpPort(optionalTimeout), processB.getExpeditionUdpPort(optionalTimeout)))); - final SshCommandChannel curlChannel = host.createRootSshChannel(optionalTimeout); + new HashSet<>(Arrays.asList(processA.getExpeditionUdpPort(optionalTimeout, privateKeyEncryptionPassphrase), processB.getExpeditionUdpPort(optionalTimeout, privateKeyEncryptionPassphrase)))); + final SshCommandChannel curlChannel = host.createRootSshChannel(optionalTimeout, privateKeyEncryptionPassphrase); final ByteArrayOutputStream stderr = new ByteArrayOutputStream(); curlChannel.sendCommandLineSynchronously( "curl -k -i -H \"Host: b.sapsailing.com\" \"https://127.0.0.1\"", stderr); @@ -154,11 +163,11 @@ public class TestProcedures { final Iterable>> replicaSets = landscape.getApplicationReplicaSetsByTag(region, sailingAnalyticsServerTag, (theHost, dir)->{ try { - return new SailingAnalyticsProcessImpl(theHost, dir, optionalTimeout); + return new SailingAnalyticsProcessImpl(theHost, dir, optionalTimeout, privateKeyEncryptionPassphrase); } catch (NumberFormatException | JSchException | IOException | InterruptedException e) { throw new RuntimeException(e); } - }, optionalTimeout); + }, optionalTimeout, privateKeyEncryptionPassphrase); // expecting to find at least "a" and "b" assertTrue(Util.containsAll(Util.map(replicaSets, ApplicationReplicaSet::getName), Arrays.asList(aServerName, bServerName))); Util.filter(replicaSets, rs->rs.getName().equals(aServerName) || rs.getName().equals(bServerName)).forEach(rs->assertTrue(Util.isEmpty(rs.getReplicas()) && rs.getMaster() != null)); @@ -200,11 +209,12 @@ public class TestProcedures { @Test public void testAddMongoReplica() throws Exception { final String keyName = "MyKey-"+UUID.randomUUID(); - landscape.createKeyPair(region, keyName); + landscape.createKeyPair(region, keyName, privateKeyEncryptionPassphrase); final StartMongoDBServer.Builder> builder = StartMongoDBServer.builder(); final StartMongoDBServer> startMongoDBServerProcedure = builder .setLandscape(landscape) .setKeyName(keyName) + .setPrivateKeyEncryptionPassphrase(privateKeyEncryptionPassphrase) .setOptionalTimeout(optionalTimeout) .build(); try { @@ -228,7 +238,7 @@ public class TestProcedures { boolean fine = true; do { try { - final SshCommandChannel sshChannel = mongoProcess.getHost().createSshChannel("ec2-user", optionalTimeout); + final SshCommandChannel sshChannel = mongoProcess.getHost().createSshChannel("ec2-user", optionalTimeout, privateKeyEncryptionPassphrase); final String stdout = sshChannel.runCommandAndReturnStdoutAndLogStderr( "i=0; while [ $i -lt $(echo \"rs.status().members.length\" | mongo 2>/dev/null | tail -n +5 | head -n +1) ]; do echo \"rs.status().members[$i].stateStr\" | mongo 2>/dev/null | tail -n +5 | head -n +1; i=$((i+1)); done", "stderr while trying to fetch replica set members", Level.WARNING); @@ -245,13 +255,14 @@ public class TestProcedures { final String keyName = "MyKey-"+UUID.randomUUID(); // Comment the following line to get default eu-west-2 test environment; uncomment to upgrade current production images in eu-west-1 // final AwsRegion region = new AwsRegion(Region.EU_WEST_1); - landscape.createKeyPair(region, keyName); + landscape.createKeyPair(region, keyName, privateKeyEncryptionPassphrase); final com.sap.sailing.landscape.procedures.UpgradeAmi.Builder> imageUpgradeProcedureBuilder = UpgradeAmi.builder(); final UpgradeAmi imageUpgradeProcedure = imageUpgradeProcedureBuilder .setLandscape(landscape) .setRegion(region) // only required in case a non-default region is used; see Landscape.getDefaultRegion() .setKeyName(keyName) + .setPrivateKeyEncryptionPassphrase(privateKeyEncryptionPassphrase) .setOptionalTimeout(optionalTimeout) .build(); try { @@ -276,7 +287,7 @@ public class TestProcedures { void testConnectivity() throws Exception { final String serverName = "test"+new Random().nextInt(); final String keyName = "MyKey-"+UUID.randomUUID(); - landscape.createKeyPair(region, keyName); + landscape.createKeyPair(region, keyName, privateKeyEncryptionPassphrase); SailingAnalyticsMasterConfiguration.Builder applicationConfigurationBuilder = SailingAnalyticsMasterConfiguration.masterBuilder(); applicationConfigurationBuilder .setServerName(serverName) @@ -292,6 +303,7 @@ public class TestProcedures { .setRegion(region) .setInstanceType(InstanceType.T3_LARGE) .setKeyName(keyName) + .setPrivateKeyEncryptionPassphrase(privateKeyEncryptionPassphrase) .setTags(Tags.with("Hello", "World")) .setOptionalTimeout(optionalTimeout) .build(); @@ -315,10 +327,10 @@ public class TestProcedures { // check env.sh access final SailingAnalyticsProcess process = startSailingAnalyticsMaster.getSailingAnalyticsProcess(); assertTrue(process.waitUntilReady(optionalTimeout)); - final String envSh = process.getEnvSh(optionalTimeout); + final String envSh = process.getEnvSh(optionalTimeout, privateKeyEncryptionPassphrase); assertFalse(envSh.isEmpty()); assertTrue("Couldn't find SERVER_NAME=\""+serverName+"\" in env.sh:\n"+envSh, envSh.contains("SERVER_NAME=\""+serverName+"\"")); - assertEquals(14888, process.getTelnetPortToOSGiConsole(optionalTimeout)); + assertEquals(14888, process.getTelnetPortToOSGiConsole(optionalTimeout, privateKeyEncryptionPassphrase)); // Now create an ALB mapping, assuming to create the dynamic ALB: final String domain = "wiesen-weg.de"; final String hostname = serverName+"."+domain; diff --git a/java/com.sap.sailing.landscape.test/src/com/sap/sailing/landscape/test/TestReleaseRepository.java b/java/com.sap.sailing.landscape.test/src/com/sap/sailing/landscape/test/TestReleaseRepository.java index 97c09a8e51e..e31b27d4889 100755 --- a/java/com.sap.sailing.landscape.test/src/com/sap/sailing/landscape/test/TestReleaseRepository.java +++ b/java/com.sap.sailing.landscape.test/src/com/sap/sailing/landscape/test/TestReleaseRepository.java @@ -9,7 +9,6 @@ import com.sap.sailing.landscape.SailingReleaseRepository; import com.sap.sse.common.Util; public class TestReleaseRepository { - @Test public void testForAtLeastOneRelease() { assertFalse(Util.isEmpty(SailingReleaseRepository.INSTANCE)); diff --git a/java/com.sap.sailing.landscape.ui/META-INF/MANIFEST.MF b/java/com.sap.sailing.landscape.ui/META-INF/MANIFEST.MF index 7bc80c1e4bd..3bc9b903359 100755 --- a/java/com.sap.sailing.landscape.ui/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.landscape.ui/META-INF/MANIFEST.MF @@ -17,7 +17,8 @@ Require-Bundle: com.sap.sse.gwt, com.sap.sailing.landscape.common, com.sap.sailing.landscape, com.sap.sse.landscape, - com.sap.sse.landscape.aws + com.sap.sse.landscape.aws, + com.amazon.aws.aws-java-api Export-Package: com.sap.sailing.landscape.ui.client, com.sap.sailing.landscape.ui.server, com.sap.sailing.landscape.ui.shared diff --git a/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/LandscapeManagementPanel.java b/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/LandscapeManagementPanel.java index cf5ba16d255..0fc8099751a 100755 --- a/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/LandscapeManagementPanel.java +++ b/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/LandscapeManagementPanel.java @@ -14,6 +14,7 @@ import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.rpc.ServiceDefTarget; import com.google.gwt.user.client.ui.CaptionPanel; import com.google.gwt.user.client.ui.Grid; +import com.google.gwt.user.client.ui.HorizontalPanel; import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.PasswordTextBox; import com.google.gwt.user.client.ui.TextBox; @@ -70,8 +71,10 @@ public class LandscapeManagementPanel extends VerticalPanel { public LandscapeManagementPanel(StringMessages stringMessages, UserService userService, AdminConsoleTableResources tableResources, ErrorReporter errorReporter) { landscapeManagementService = initAndRegisterLandscapeManagementService(); + final HorizontalPanel awsCredentialsAndSshKeys = new HorizontalPanel(); + add(awsCredentialsAndSshKeys); final CaptionPanel awsCredentialsPanel = new CaptionPanel(stringMessages.awsCredentials()); - add(awsCredentialsPanel); + awsCredentialsAndSshKeys.add(awsCredentialsPanel); final Grid awsCredentialsGrid = new Grid(2, 2); awsCredentialsPanel.add(awsCredentialsGrid); awsCredentialsGrid.setWidget(0, 0, new Label(stringMessages.awsAccessKey())); @@ -101,6 +104,10 @@ public class LandscapeManagementPanel extends VerticalPanel { awsCredentialsGrid.setWidget(1, 0, new Label(stringMessages.awsSecret())); awsSecretPasswordTextBox = new PasswordTextBox(); awsCredentialsGrid.setWidget(1, 1, awsSecretPasswordTextBox); + final SshKeyManagementPanel sshKeyManagementPanel = new SshKeyManagementPanel(stringMessages, userService, landscapeManagementService, tableResources, errorReporter); + final CaptionPanel sshKeysCaptionPanel = new CaptionPanel(stringMessages.sshKeys()); + awsCredentialsAndSshKeys.add(sshKeysCaptionPanel); + sshKeysCaptionPanel.add(sshKeyManagementPanel); regionsTable = new TableWrapperWithSingleSelectionAndFilter( stringMessages, errorReporter, /* enablePager */ false, /* entity identity comparator */ Optional.empty(), GWT.create(AdminConsoleTableResources.class), @@ -164,6 +171,8 @@ public class LandscapeManagementPanel extends VerticalPanel { add(mongoEndpointsBusy); regionsTable.getSelectionModel().addSelectionChangeHandler(e-> { + sshKeyManagementPanel.showKeysInRegion(awsAccessKeyTextBox.getValue(), awsSecretPasswordTextBox.getValue(), + regionsTable.getSelectionModel().getSelectedObject()); mongoEndpointsTable.getDataProvider().getList().clear(); if (regionsTable.getSelectionModel().getSelectedObject() != null) { mongoEndpointsBusy.setBusy(true); @@ -185,16 +194,17 @@ public class LandscapeManagementPanel extends VerticalPanel { mongoEndpointsTable.getDataProvider().getList().clear(); } }); - // TODO region could be a drop-down maybe - // TODO upon region selection show MongoDB, RabbitMQ, AppServer and Central Reverse Proxy clusters, and upgradable AMIs in region + // TODO support SSH key management: show existing keys, allow user to paste an encrypted private key for storage in MongoDB; allow user to upload public key and choose for launch and login procedures + // TODO upon region selection show AppServer clusters, and upgradable AMIs in region // TODO try to identify archive servers // TODO support creating a new app server cluster // TODO support AMI upgrade - // TODO support archiving and dismantling of an application server cluster // TODO support archive server upgrade // TODO support upgrading all app server instances in a region + // TODO region could be a drop-down maybe, and it could remember its last selection + // TODO upon region selection show RabbitMQ, and Central Reverse Proxy clusters in region + // TODO support archiving and dismantling of an application server cluster // TODO support deploying a new app server process instance onto an existing app server host (multi-instance) - // TODO support SSH key management: show existing keys, allow user to paste an encrypted private key for storage in MongoDB; allow user to upload public key and choose for launch and login procedures } private LandscapeManagementWriteServiceAsync initAndRegisterLandscapeManagementService() { diff --git a/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/LandscapeManagementWriteService.java b/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/LandscapeManagementWriteService.java index 4c50fcfe168..9492c580562 100755 --- a/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/LandscapeManagementWriteService.java +++ b/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/LandscapeManagementWriteService.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import com.google.gwt.user.client.rpc.RemoteService; import com.sap.sailing.landscape.ui.shared.MongoEndpointDTO; +import com.sap.sailing.landscape.ui.shared.SSHKeyPairDTO; public interface LandscapeManagementWriteService extends RemoteService { ArrayList getRegions(); @@ -11,4 +12,6 @@ public interface LandscapeManagementWriteService extends RemoteService { ArrayList getMongoEndpoints(String awsAccessKey, String awsSecret, String region); MongoEndpointDTO getMongoEndpoint(String awsAccessKey, String awsSecret, String region, String replicaSetName); + + ArrayList getSshKeys(String awsAccessKey, String awsSecret, String regionId); } diff --git a/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/LandscapeManagementWriteServiceAsync.java b/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/LandscapeManagementWriteServiceAsync.java index 3539d3a6b9d..762d81b5a6d 100755 --- a/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/LandscapeManagementWriteServiceAsync.java +++ b/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/LandscapeManagementWriteServiceAsync.java @@ -4,11 +4,17 @@ import java.util.ArrayList; import com.google.gwt.user.client.rpc.AsyncCallback; import com.sap.sailing.landscape.ui.shared.MongoEndpointDTO; +import com.sap.sailing.landscape.ui.shared.SSHKeyPairDTO; public interface LandscapeManagementWriteServiceAsync { void getRegions(AsyncCallback> asyncCallback); - void getMongoEndpoints(String awsAccessKey, String awsSecret, String region, AsyncCallback> callback); + void getMongoEndpoints(String awsAccessKey, String awsSecret, String regionId, + AsyncCallback> callback); - void getMongoEndpoint(String awsAccessKey, String awsSecret, String region, String replicaSetName, AsyncCallback callback); + void getMongoEndpoint(String awsAccessKey, String awsSecret, String region, String replicaSetName, + AsyncCallback callback); + + void getSshKeys(String awsAccessKey, String awsSecret, String regionId, + AsyncCallback> callback); } diff --git a/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/SshKeyManagementPanel.java b/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/SshKeyManagementPanel.java new file mode 100755 index 00000000000..f861d4d3e95 --- /dev/null +++ b/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/SshKeyManagementPanel.java @@ -0,0 +1,81 @@ +package com.sap.sailing.landscape.ui.client; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Optional; + +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.ui.VerticalPanel; +import com.sap.sailing.landscape.ui.client.i18n.StringMessages; +import com.sap.sailing.landscape.ui.shared.SSHKeyPairDTO; +import com.sap.sse.common.Util; +import com.sap.sse.gwt.adminconsole.AdminConsoleTableResources; +import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; +import com.sap.sse.gwt.client.celltable.TableWrapperWithSingleSelectionAndFilter; +import com.sap.sse.gwt.client.controls.busyindicator.BusyIndicator; +import com.sap.sse.gwt.client.controls.busyindicator.SimpleBusyIndicator; +import com.sap.sse.security.ui.client.UserService; + +public class SshKeyManagementPanel extends VerticalPanel { + private final LandscapeManagementWriteServiceAsync landscapeManagementService; + private final TableWrapperWithSingleSelectionAndFilter sshKeyTable; + private final BusyIndicator sshKeyLoadingBusy; + private final ErrorReporter errorReporter; + + public SshKeyManagementPanel(StringMessages stringMessages, UserService userService, + LandscapeManagementWriteServiceAsync landscapeManagementService, AdminConsoleTableResources tableResources, + ErrorReporter errorReporter) { + this.landscapeManagementService = landscapeManagementService; + this.errorReporter = errorReporter; + sshKeyTable = + new TableWrapperWithSingleSelectionAndFilter(stringMessages, errorReporter, /* enablePager */ true, + Optional.of(new EntityIdentityComparator() { + @Override + public boolean representSameEntity(SSHKeyPairDTO dto1, SSHKeyPairDTO dto2) { + return Util.equalsWithNull(dto1.getRegionId(), dto2.getRegionId()) && + Util.equalsWithNull(dto1.getName(), dto2.getName()); + } + + @Override + public int hashCode(SSHKeyPairDTO t) { + return (t.getRegionId() == null ? 0 : t.getRegionId().hashCode()) ^ + (t.getName() == null ? 0 : t.getName().hashCode()); + } + }), tableResources, Optional.empty(), /* filter label */ Optional.empty(), /* filter checkbox label */ null) { + @Override + protected Iterable getSearchableStrings(SSHKeyPairDTO t) { + return Arrays.asList(t.getName(), t.getRegionId(), t.getCreatorName()); + } + }; + sshKeyTable.addColumn(object->object.getRegionId(), stringMessages.region()); + sshKeyTable.addColumn(object->object.getName(), stringMessages.name()); + sshKeyTable.addColumn(object->object.getCreatorName(), stringMessages.creator()); + sshKeyTable.addColumn(object->object.getCreationTime().toString(), stringMessages.creationTime()); + add(sshKeyTable); + sshKeyLoadingBusy = new SimpleBusyIndicator(); + add(sshKeyLoadingBusy); + } + + public void showKeysInRegion(String awsAccessKey, String awsSecret, String regionId) { + sshKeyTable.getDataProvider().getList().clear(); + if (regionId != null) { + sshKeyLoadingBusy.setBusy(true); + landscapeManagementService.getSshKeys(awsAccessKey, awsSecret, regionId, new AsyncCallback>() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError(caught.getMessage()); + sshKeyLoadingBusy.setBusy(false); + } + + @Override + public void onSuccess(ArrayList sshKeyPairDTOs) { + sshKeyTable.refresh(sshKeyPairDTOs); + sshKeyLoadingBusy.setBusy(false); + } + }); + } else { + sshKeyTable.getDataProvider().getList().clear(); + } + } +} diff --git a/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/i18n/StringMessages.java b/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/i18n/StringMessages.java index e2d104bc514..dbc307639d6 100755 --- a/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/i18n/StringMessages.java +++ b/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/i18n/StringMessages.java @@ -17,4 +17,7 @@ com.sap.sse.gwt.adminconsole.StringMessages { String awsCredentials(); String awsAccessKey(); String awsSecret(); + String creator(); + String creationTime(); + String sshKeys(); } diff --git a/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/i18n/StringMessages.properties b/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/i18n/StringMessages.properties index d505d350221..e7567687707 100755 --- a/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/i18n/StringMessages.properties +++ b/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/i18n/StringMessages.properties @@ -5,4 +5,7 @@ upgrade=Upgrade replicaSet=Replica Set awsCredentials=AWS Credentials awsAccessKey=AWS Access Key -awsSecret=AWS Secret \ No newline at end of file +awsSecret=AWS Secret +creator=Creator +creationTime=Creation Time +sshKeys=SSH Keys \ No newline at end of file diff --git a/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/i18n/StringMessages_de.properties b/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/i18n/StringMessages_de.properties index e9b315d3b9f..6ddc987ebaa 100755 --- a/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/i18n/StringMessages_de.properties +++ b/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/i18n/StringMessages_de.properties @@ -5,4 +5,7 @@ upgrade=Aktualisieren replicaSet=Replica-Set awsCredentials=AWS Zugangsdaten awsAccessKey=AWS Zugangsschlüssel -awsSecret=AWS Geheimnis \ No newline at end of file +awsSecret=AWS Geheimnis +creator=Ersteller +creationTime=Erstellungszeitpunkt +sshKeys=SSH-Schlüssel \ No newline at end of file diff --git a/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/server/LandscapeManagementWriteServiceImpl.java b/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/server/LandscapeManagementWriteServiceImpl.java index b3f72367687..785d1e553ff 100755 --- a/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/server/LandscapeManagementWriteServiceImpl.java +++ b/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/server/LandscapeManagementWriteServiceImpl.java @@ -3,17 +3,25 @@ package com.sap.sailing.landscape.ui.server; import java.util.ArrayList; import java.util.Collections; +import org.apache.shiro.SecurityUtils; + import com.sap.sailing.landscape.ui.client.LandscapeManagementWriteService; import com.sap.sailing.landscape.ui.shared.MongoEndpointDTO; +import com.sap.sailing.landscape.ui.shared.SSHKeyPairDTO; import com.sap.sse.common.Util; import com.sap.sse.common.Util.Pair; import com.sap.sse.gwt.server.ResultCachingProxiedRemoteServiceServlet; +import com.sap.sse.landscape.application.ApplicationProcessMetrics; import com.sap.sse.landscape.aws.AwsLandscape; import com.sap.sse.landscape.aws.impl.AwsRegion; import com.sap.sse.landscape.mongodb.MongoEndpoint; import com.sap.sse.landscape.mongodb.MongoProcess; import com.sap.sse.landscape.mongodb.MongoProcessInReplicaSet; import com.sap.sse.landscape.mongodb.MongoReplicaSet; +import com.sap.sse.landscape.ssh.SSHKeyPair; +import com.sap.sse.security.shared.impl.SecuredSecurityTypes; + +import software.amazon.awssdk.services.ec2.model.KeyPairInfo; public class LandscapeManagementWriteServiceImpl extends ResultCachingProxiedRemoteServiceServlet implements LandscapeManagementWriteService { @@ -24,6 +32,7 @@ public class LandscapeManagementWriteServiceImpl extends ResultCachingProxiedRem @Override public ArrayList getRegions() { + SecurityUtils.getSubject().checkPermission(SecuredSecurityTypes.LANDSCAPE.getStringPermission(SecuredSecurityTypes.LandscapeActions.MANAGE)); final ArrayList result = new ArrayList<>(); Util.addAll(Util.map(AwsLandscape.obtain().getRegions(), r->r.getId()), result); return result; @@ -31,6 +40,7 @@ public class LandscapeManagementWriteServiceImpl extends ResultCachingProxiedRem @Override public ArrayList getMongoEndpoints(String awsAccessKey, String awsSecret, String region) { + SecurityUtils.getSubject().checkPermission(SecuredSecurityTypes.LANDSCAPE.getStringPermission(SecuredSecurityTypes.LandscapeActions.MANAGE)); final ArrayList result = new ArrayList<>(); for (final MongoEndpoint mongoEndpoint : AwsLandscape.obtain(awsAccessKey, awsSecret).getMongoEndpoints(new AwsRegion(region))) { final MongoEndpointDTO dto; @@ -54,4 +64,18 @@ public class LandscapeManagementWriteServiceImpl extends ResultCachingProxiedRem public MongoEndpointDTO getMongoEndpoint(String awsAccessKey, String awsSecret, String region, String replicaSetName) { return getMongoEndpoints(awsAccessKey, awsSecret, region).stream().filter(mep->Util.equalsWithNull(mep.getReplicaSetName(), replicaSetName)).findAny().orElse(null); } + + @Override + public ArrayList getSshKeys(String awsAccessKey, String awsSecret, String regionId) { + final ArrayList result = new ArrayList<>(); + final AwsLandscape landscape = AwsLandscape.obtain(awsAccessKey, awsSecret); + final AwsRegion region = new AwsRegion(regionId); + for (final KeyPairInfo keyPairInfo : landscape.getAllKeyPairInfos(region)) { + final SSHKeyPair key = landscape.getSSHKeyPair(region, keyPairInfo.keyName()); + if (key != null) { + result.add(new SSHKeyPairDTO(key.getRegionId(), key.getName(), key.getCreatorName(), key.getCreationTime())); + } + } + return result; + } } diff --git a/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/shared/SSHKeyPairDTO.java b/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/shared/SSHKeyPairDTO.java new file mode 100755 index 00000000000..b5da8b1ef77 --- /dev/null +++ b/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/shared/SSHKeyPairDTO.java @@ -0,0 +1,40 @@ +package com.sap.sailing.landscape.ui.shared; + +import com.google.gwt.user.client.rpc.IsSerializable; +import com.sap.sse.common.TimePoint; + +public class SSHKeyPairDTO implements IsSerializable { + private String regionId; + private String name; + private String creatorName; + private TimePoint creationTime; + + @Deprecated + SSHKeyPairDTO() { + // for GWT RPC serialization only + } + + public SSHKeyPairDTO(String regionId, String name, String creatorName, TimePoint creationTime) { + super(); + this.regionId = regionId; + this.name = name; + this.creatorName = creatorName; + this.creationTime = creationTime; + } + + public String getRegionId() { + return regionId; + } + + public String getName() { + return name; + } + + public String getCreatorName() { + return creatorName; + } + + public TimePoint getCreationTime() { + return creationTime; + } +} diff --git a/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/SailingAnalyticsProcess.java b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/SailingAnalyticsProcess.java index 3de44357ad4..c537ae7791a 100755 --- a/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/SailingAnalyticsProcess.java +++ b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/SailingAnalyticsProcess.java @@ -13,7 +13,9 @@ public interface SailingAnalyticsProcess extends ApplicationProcess static Logger logger = Logger.getLogger(SailingAnalyticsProcess.class.getName()); static String HEALTH_CHECK_PATH = "/gwt/status"; - default int getExpeditionUdpPort(Optional optionalTimeout) throws NumberFormatException, JSchException, IOException, InterruptedException, SftpException { - return Integer.parseInt(getEnvShValueFor(SailingAnalyticsProcessConfigurationVariable.EXPEDITION_PORT.name(), optionalTimeout)); + default int getExpeditionUdpPort(Optional optionalTimeout, byte[] privateKeyEncryptionPassphrase) + throws NumberFormatException, JSchException, IOException, InterruptedException, SftpException { + return Integer.parseInt(getEnvShValueFor(SailingAnalyticsProcessConfigurationVariable.EXPEDITION_PORT.name(), + optionalTimeout, privateKeyEncryptionPassphrase)); } } diff --git a/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/impl/SailingAnalyticsProcessImpl.java b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/impl/SailingAnalyticsProcessImpl.java index 6a52212b3ce..960cf8ebf18 100755 --- a/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/impl/SailingAnalyticsProcessImpl.java +++ b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/impl/SailingAnalyticsProcessImpl.java @@ -32,9 +32,10 @@ implements SailingAnalyticsProcess { /** * Tries to obtain the port from the {@code env.sh} file found in the {@code serverDirectory} */ - public SailingAnalyticsProcessImpl(Host host, String serverDirectory, Optional optionalTimeout) + public SailingAnalyticsProcessImpl(Host host, String serverDirectory, Optional optionalTimeout, + byte[] privateKeyEncryptionPassphrase) throws NumberFormatException, JSchException, IOException, InterruptedException { - super(host, serverDirectory, optionalTimeout); + super(host, serverDirectory, optionalTimeout, privateKeyEncryptionPassphrase); } public SailingAnalyticsProcessImpl(int port, Host host, String serverDirectory) { @@ -60,7 +61,7 @@ implements SailingAnalyticsProcess { * information about server name as well as availability and replication status can be obtained. */ @Override - public String getServerName(Optional optionalTimeout) + public String getServerName(Optional optionalTimeout, byte[] privateKeyEncryptionPassphrase) throws JSchException, IOException, InterruptedException, SftpException, ParseException { String result = null; final TimePoint start = TimePoint.now(); diff --git a/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/procedures/DeployProcessOnMultiServer.java b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/procedures/DeployProcessOnMultiServer.java index 67ce292f376..4952af4dafa 100755 --- a/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/procedures/DeployProcessOnMultiServer.java +++ b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/procedures/DeployProcessOnMultiServer.java @@ -45,6 +45,7 @@ implements Procedure> hostToDeployTo; private final ApplicationConfigurationT applicationConfiguration; private final Optional optionalTimeout; + private byte[] privateKeyEncryptionPassphrase; /** * The process launched by this procedure. {@link #hostToDeployTo} is expected to be identical to @@ -88,6 +89,7 @@ implements Procedure> extends com.sap.sse.landscape.orchestration.Procedure.Builder, ShardingKey, SailingAnalyticsMetrics, SailingAnalyticsProcess> { BuilderT setHostToDeployTo(ApplicationProcessHost> hostToDeployTo); + BuilderT setPrivateKeyEncryptionPassphrase(byte[] privateKeyEncryptionPassphrase); } public static class BuilderImpl, ShardingKey, HostT extends AwsInstance, @@ -97,6 +99,7 @@ implements Procedure { private final SailingAnalyticsApplicationConfiguration.BuilderImpl applicationConfigurationBuilder; private ApplicationProcessHost> hostToDeployTo; + private byte[] privateKeyEncryptionPassphrase; protected BuilderImpl(SailingAnalyticsApplicationConfiguration.BuilderImpl applicationConfigurationBuilder) { super(); @@ -110,7 +113,7 @@ implements Procedure> applicationProcesses = getHostToDeployTo().getApplicationProcesses(getOptionalTimeout()); + final Iterable> applicationProcesses = getHostToDeployTo().getApplicationProcesses(getOptionalTimeout(), privateKeyEncryptionPassphrase); if (!getApplicationConfigurationBuilder().isPortSet()) { getApplicationConfigurationBuilder().setPort(getNextAvailablePort(applicationProcesses, SailingAnalyticsApplicationConfiguration.Builder.DEFAULT_PORT, @@ -121,7 +124,7 @@ implements Procedure{ try { - return ap.getTelnetPortToOSGiConsole(getOptionalTimeout()); + return ap.getTelnetPortToOSGiConsole(getOptionalTimeout(), privateKeyEncryptionPassphrase); } catch (NumberFormatException | JSchException | IOException | SftpException | InterruptedException e) { throw new RuntimeException(e); @@ -133,7 +136,7 @@ implements Procedure{ try { - return ap.getExpeditionUdpPort(getOptionalTimeout()); + return ap.getExpeditionUdpPort(getOptionalTimeout(), privateKeyEncryptionPassphrase); } catch (NumberFormatException | JSchException | IOException | InterruptedException | SftpException e) { throw new RuntimeException(e); @@ -197,6 +200,16 @@ implements Procedure getOptionalTimeout() { return super.getOptionalTimeout(); } + + @Override + public BuilderT setPrivateKeyEncryptionPassphrase(byte[] privateKeyEncryptionPassphrase) { + this.privateKeyEncryptionPassphrase = privateKeyEncryptionPassphrase; + return self(); + } + + protected byte[] getPrivateKeyEncryptionPassphrase() { + return privateKeyEncryptionPassphrase; + } } public static , ShardingKey, HostT extends AwsInstance, @@ -213,6 +226,7 @@ implements Procedure(applicationConfiguration.getPort(), getHostToDeployTo(), serverDirectory); diff --git a/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/procedures/StartMultiServer.java b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/procedures/StartMultiServer.java index 9f603c4d874..a965f2fef5a 100755 --- a/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/procedures/StartMultiServer.java +++ b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/procedures/StartMultiServer.java @@ -96,7 +96,7 @@ implements StartFromSailingAnalyticsImage { result = (instanceId, az, landscape)-> new ApplicationProcessHostImpl>(instanceId, az, landscape, (host, serverDirectory)->{ try { - return new SailingAnalyticsProcessImpl(host, serverDirectory, getOptionalTimeout()); + return new SailingAnalyticsProcessImpl(host, serverDirectory, getOptionalTimeout(), getPrivateKeyEncryptionPassphrase()); } catch (NumberFormatException | JSchException | IOException | InterruptedException e) { throw new RuntimeException(e); } @@ -141,11 +141,11 @@ implements StartFromSailingAnalyticsImage { super.run(); // this will trigger the "sailing" init.d script running in the background, triggering the image upgrade, then the httpd stop and clean-up copyRootAuthorizedKeysToOtherUser(SAILING_USER_NAME, optionalTimeout); final String instanceId = getHost().getInstanceId(); - getHost().getReverseProxy().createInternalStatusRedirect(optionalTimeout); + getHost().getReverseProxy().createInternalStatusRedirect(optionalTimeout, getPrivateKeyEncryptionPassphrase()); boolean fileFound = false; final TimePoint startedToWaitForImageUpgradeToFinish = TimePoint.now(); do { - final ChannelSftp sftpChannel = getHost().createRootSftpChannel(optionalTimeout); + final ChannelSftp sftpChannel = getHost().createRootSftpChannel(optionalTimeout, getPrivateKeyEncryptionPassphrase()); sftpChannel.connect(); try { final SftpATTRS stat = sftpChannel.stat("/tmp/image-upgrade-finished"); @@ -160,7 +160,7 @@ implements StartFromSailingAnalyticsImage { // wait until the file indicating the finishing of the image upgrade process was found // or, if a timeout was provided, the timeout expired } while (!fileFound && optionalTimeout.map(timeout->startedToWaitForImageUpgradeToFinish.plus(timeout).after(TimePoint.now())).orElse(true)); - final SshCommandChannel sshCommandChannel = getHost().createRootSshChannel(optionalTimeout); + final SshCommandChannel sshCommandChannel = getHost().createRootSshChannel(optionalTimeout, getPrivateKeyEncryptionPassphrase()); logger.info("stdout for removing "+ApplicationProcessHost.DEFAULT_SERVER_PATH+" and starting httpd service on instance "+instanceId+": "+ sshCommandChannel.runCommandAndReturnStdoutAndLogStderr("rm -rf "+ApplicationProcessHost.DEFAULT_SERVER_PATH+"; service httpd start", "stderr for removing "+ApplicationProcessHost.DEFAULT_SERVER_PATH+" and starting httpd service on instance \"+instanceId+\": ", Level.INFO)); diff --git a/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/procedures/StartSailingAnalyticsHost.java b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/procedures/StartSailingAnalyticsHost.java index 1dcff5f66a8..bf65e408d19 100755 --- a/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/procedures/StartSailingAnalyticsHost.java +++ b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/procedures/StartSailingAnalyticsHost.java @@ -93,7 +93,7 @@ implements Procedure(instanceId, az, landscape, (host, serverDirectory)->{ try { - return new SailingAnalyticsProcessImpl(host, serverDirectory, getOptionalTimeout()); + return new SailingAnalyticsProcessImpl(host, serverDirectory, getOptionalTimeout(), getPrivateKeyEncryptionPassphrase()); } catch (NumberFormatException | JSchException | IOException | InterruptedException e) { throw new RuntimeException(e); } diff --git a/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/procedures/UpgradeAmi.java b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/procedures/UpgradeAmi.java index 08d311a125e..daed83d634d 100755 --- a/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/procedures/UpgradeAmi.java +++ b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/procedures/UpgradeAmi.java @@ -208,7 +208,7 @@ implements Procedure(instanceId, az, landscape, (host, serverDirectory)->{ try { - return new SailingAnalyticsProcessImpl(host, serverDirectory, getOptionalTimeout()); + return new SailingAnalyticsProcessImpl(host, serverDirectory, getOptionalTimeout(), getPrivateKeyEncryptionPassphrase()); } catch (NumberFormatException | JSchException | IOException | InterruptedException e) { throw new RuntimeException(e); } diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/TableWrapper.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/TableWrapper.java index f14a79e43fa..659b308d61f 100755 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/TableWrapper.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/TableWrapper.java @@ -1,11 +1,13 @@ package com.sap.sse.gwt.client.celltable; import java.util.Comparator; +import java.util.function.Function; import com.google.gwt.core.client.GWT; import com.google.gwt.user.cellview.client.Column; import com.google.gwt.user.cellview.client.ColumnSortEvent.ListHandler; import com.google.gwt.user.cellview.client.SimplePager; +import com.google.gwt.user.cellview.client.TextColumn; import com.google.gwt.user.client.ui.IsWidget; import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwt.user.client.ui.Widget; @@ -14,6 +16,7 @@ import com.google.gwt.view.client.ListDataProvider; import com.google.gwt.view.client.MultiSelectionModel; import com.google.gwt.view.client.Range; import com.sap.sse.common.Util; +import com.sap.sse.common.util.NaturalComparator; import com.sap.sse.gwt.client.ErrorReporter; import com.sap.sse.gwt.client.StringMessages; import com.sap.sse.gwt.client.panels.AbstractFilterablePanel; @@ -125,6 +128,20 @@ public abstract class TableWrapper, SM boatColumnListHandler.setComparator(column, comparator); addColumn(column, header); } + + /** + * Adds a sortable {@link TextColumn} whose {@link TextColumn#getValue(Object)} method is based on the {@code valueMapper} + * and whose sorting is based on a {@link NaturalComparator}. + */ + public void addColumn(Function valueMapper, String header) { + final TextColumn textColumn = new TextColumn() { + @Override + public String getValue(T object) { + return valueMapper.apply(object); + } + }; + addColumn(textColumn, header, Comparator.comparing(t->textColumn.getValue(t), new NaturalComparator())); + } public void setEmptyTableWidget(Widget widget) { table.setEmptyTableWidget(widget); diff --git a/java/com.sap.sse.landscape.aws.test/src/com/sap/sse/landscape/aws/ConnectivityTest.java b/java/com.sap.sse.landscape.aws.test/src/com/sap/sse/landscape/aws/ConnectivityTest.java index a910e0d007d..24d4ee68d0a 100755 --- a/java/com.sap.sse.landscape.aws.test/src/com/sap/sse/landscape/aws/ConnectivityTest.java +++ b/java/com.sap.sse.landscape.aws.test/src/com/sap/sse/landscape/aws/ConnectivityTest.java @@ -15,6 +15,7 @@ import java.net.InetAddress; import java.net.URL; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.util.Base64; import java.util.Collections; import java.util.Optional; import java.util.Random; @@ -23,7 +24,6 @@ import java.util.logging.Level; import java.util.logging.Logger; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import com.jcraft.jsch.ChannelSftp; @@ -32,6 +32,7 @@ import com.jcraft.jsch.JSchException; import com.jcraft.jsch.KeyPair; import com.jcraft.jsch.SftpException; import com.sap.sailing.landscape.SailingAnalyticsMetrics; +import com.sap.sailing.landscape.SailingReleaseRepository; import com.sap.sailing.landscape.impl.SailingAnalyticsProcessImpl; import com.sap.sailing.landscape.procedures.StartFromSailingAnalyticsImage; import com.sap.sse.common.Duration; @@ -43,6 +44,8 @@ import com.sap.sse.landscape.application.ApplicationProcess; import com.sap.sse.landscape.aws.impl.AwsRegion; import com.sap.sse.landscape.aws.orchestration.CreateDNSBasedLoadBalancerMapping; import com.sap.sse.landscape.impl.ReleaseRepositoryImpl; +import com.sap.sse.landscape.mongodb.MongoEndpoint; +import com.sap.sse.landscape.mongodb.impl.DatabaseImpl; import com.sap.sse.landscape.ssh.SSHKeyPair; import com.sap.sse.landscape.ssh.SshCommandChannel; @@ -72,26 +75,72 @@ public class ConnectivityTest landscape; private AwsRegion region; private byte[] keyPass; + private String AXELS_KEY_PASS; @Before public void setUp() { landscape = AwsLandscape.obtain(); region = new AwsRegion(Region.EU_WEST_2); + AXELS_KEY_PASS = new String(Base64.getDecoder().decode(System.getProperty("axelskeypassphrase"))); keyPass = "lkayrelakuesyrlasp8caorewyc".getBytes(); } - @Ignore("Fill in key details for the key used to launch the central reverse proxy in the test landscape") + //@Ignore("Fill in key details for the key used to launch the central reverse proxy in the test landscape") @Test public void readAndStoreSSHKey() throws JSchException, IOException, InterruptedException { - final String AXELS_KEY_NAME = "The key name goes here"; - final String KEY_PASSPHRASE = "your passphrase goes here"; - final String PATH_TO_YOUR_PRIVATE_KEY = "the path to your id_rsa file"; + final String AXELS_KEY_NAME = "Axel"; + final String PATH_TO_YOUR_PRIVATE_KEY = "c:/Users/d043530/.ssh/id_rsa"; landscape.deleteKeyPair(region, AXELS_KEY_NAME); final KeyPair keyPair = KeyPair.load(new JSch(), PATH_TO_YOUR_PRIVATE_KEY, PATH_TO_YOUR_PRIVATE_KEY+".pub"); final byte[] pubKeyBytes = getPublicKeyBytes(keyPair); - final byte[] privKeyBytes = getPrivateKeyBytes(keyPair, KEY_PASSPHRASE.getBytes()); - landscape.importKeyPair(region, pubKeyBytes, privKeyBytes, AXELS_KEY_NAME); - final SshCommandChannel sshChannel = landscape.getCentralReverseProxy(region).getHosts().iterator().next().createRootSshChannel(optionalTimeout); + // WORKS: + // 1) decrypt private key read from file containing encrypted key + // 2) obtain bytes of private key unencrypted + // 3) "load"/create new KeyPair from private key decrypted this way + // 4) encrypt private key based on KeyPair loaded from decrypted private key + // 5) "load"/create new KeyPair from key *encrypted* this way + // 6) decryption of private key works + final ByteArrayOutputStream privateKey = new ByteArrayOutputStream(); + keyPair.decrypt(AXELS_KEY_PASS.getBytes()); + keyPair.writePrivateKey(privateKey); // writing plain unencrypted private key WORKS + final KeyPair keyPairReadFromFileAndDescrypted = KeyPair.load(new JSch(), privateKey.toByteArray(), pubKeyBytes); + assertFalse(keyPairReadFromFileAndDescrypted.isEncrypted()); + final ByteArrayOutputStream privateKeyEncrypted = new ByteArrayOutputStream(); + keyPairReadFromFileAndDescrypted.writePrivateKey(privateKeyEncrypted, AXELS_KEY_PASS.getBytes()); + final KeyPair toAndFrom = KeyPair.load(new JSch(), privateKeyEncrypted.toByteArray(), pubKeyBytes); + assertTrue(toAndFrom.isEncrypted()); + assertTrue(toAndFrom.decrypt(AXELS_KEY_PASS.getBytes())); + assertFalse(toAndFrom.isEncrypted()); + // WORKS: + // 1) decrypt private key read from file containing encrypted key using getDecryptedPrivateKeyBytes(...) + // 2) obtain bytes of private key unencrypted + // 3) "load"/create new KeyPair from private key decrypted this way + // 4) encrypt private key based on KeyPair loaded from encrypted private key + // 5) "load"/create new KeyPair from key *encrypted* this way + // 6) decryption of private key works + final byte[] privKeyBytesDecrypted = getDecryptedPrivateKeyBytes(keyPair, AXELS_KEY_PASS.getBytes()); + final KeyPair testKeyFromDecryptedBytes = KeyPair.load(new JSch(), privKeyBytesDecrypted, pubKeyBytes); + assertFalse(testKeyFromDecryptedBytes.isEncrypted()); + final ByteArrayOutputStream encryptedPrivateKeyFromDecrypted = new ByteArrayOutputStream(); + testKeyFromDecryptedBytes.writePrivateKey(encryptedPrivateKeyFromDecrypted, AXELS_KEY_PASS.getBytes()); + final KeyPair testKeyFromEncryptedBytes = KeyPair.load(new JSch(), encryptedPrivateKeyFromDecrypted.toByteArray(), pubKeyBytes); + assertTrue(testKeyFromEncryptedBytes.isEncrypted()); + assertTrue(testKeyFromEncryptedBytes.decrypt(AXELS_KEY_PASS.getBytes())); + assertFalse(testKeyFromEncryptedBytes.isEncrypted()); + // BROKEN: + // 1) decrypt private key read from file containing encrypted key + // 2) obtain bytes of private key *encrypted* (writePrivateKey with non-null passphrase) + // 3) "load"/create new KeyPair from *encrypted* private key + // 4) decryption of private key FAILS + // We manifest this BROKENNESS here in this test case: + final byte[] privKeyBytesEncrypted = getPrivateKeyBytes(keyPair, AXELS_KEY_PASS.getBytes()); + final KeyPair testKeyFromBytes = KeyPair.load(new JSch(), privKeyBytesEncrypted, pubKeyBytes); + assertTrue(testKeyFromBytes.isEncrypted()); + assertFalse(testKeyFromBytes.decrypt(AXELS_KEY_PASS.getBytes())); // THIS IS THE STRANGE CASE; I THINK IT SHOULD HAVE WORKED! + assertTrue(testKeyFromBytes.isEncrypted()); // AND AS A CONSEQUENCE THE KEY IS STILL ENCRYPTED! + // In order to use this to import a valid key existing pair we'll import the last valid key, namely testKeyFromEncryptedBytes: + landscape.importKeyPair(region, pubKeyBytes, encryptedPrivateKeyFromDecrypted.toByteArray(), AXELS_KEY_NAME); + final SshCommandChannel sshChannel = landscape.getCentralReverseProxy(region).getHosts().iterator().next().createRootSshChannel(optionalTimeout, AXELS_KEY_PASS.getBytes()); final String stdout = sshChannel.runCommandAndReturnStdoutAndLogStderr("ls -al", /* stderr prefix */ null, /* stderr log level */ null); assertFalse(stdout.isEmpty()); } @@ -105,16 +154,16 @@ public class ConnectivityTest proxyHost = proxy.getHosts().iterator().next(); final ProcessT process = createApplicationProcess(proxyHost); - proxy.setEventRedirect(hostname, process, UUID.randomUUID()); + proxy.setEventRedirect(hostname, process, UUID.randomUUID(), AXELS_KEY_PASS.getBytes()); final ByteArrayOutputStream configFileContents = new ByteArrayOutputStream(); - final ChannelSftp sftpChannel = proxyHost.createRootSftpChannel(optionalTimeout); + final ChannelSftp sftpChannel = proxyHost.createRootSftpChannel(optionalTimeout, AXELS_KEY_PASS.getBytes()); sftpChannel.connect((int) optionalTimeout.orElse(Duration.NULL).asMillis()); final String configFileName = "/etc/httpd/conf.d/"+hostname+".conf"; sftpChannel.get(configFileName, configFileContents); assertTrue(configFileContents.toString().startsWith("Use Event-SSL "+hostname)); sftpChannel.disconnect(); - proxy.removeRedirect(hostname); - final SshCommandChannel lsSshChannel = proxyHost.createRootSshChannel(optionalTimeout); + proxy.removeRedirect(hostname, AXELS_KEY_PASS.getBytes()); + final SshCommandChannel lsSshChannel = proxyHost.createRootSshChannel(optionalTimeout, AXELS_KEY_PASS.getBytes()); final String lsOutput = lsSshChannel.runCommandAndReturnStdoutAndLogStderr("ls "+configFileName, /* stderr prefix */ null, /* stderr log level */ null); assertTrue(lsOutput.isEmpty()); } @@ -133,10 +182,13 @@ public class ConnectivityTest host = landscape.launchHost( getLatestSailingImage(), InstanceType.T3_SMALL, landscape.getAvailabilityZoneByName(region, "eu-west-2b"), keyName, Collections.singleton(()->"sg-0b2afd48960251280"), - Optional.of(Tags.with("Name", "MyHost").and("Hello", "World"))); + Optional.of(Tags.with("Name", "MyHost").and("Hello", "World")), + "MONGODB_URI=\""+mongoEndpoint.getURI(Optional.of(new DatabaseImpl(mongoEndpoint, "winddbTest")))+"\"", + "INSTALL_FROM_RELEASE="+SailingReleaseRepository.INSTANCE.getLatestRelease("bug4811").getName()); try { assertNotNull(host); final Instance instance = landscape.getInstance(host.getInstanceId(), region); @@ -155,12 +207,12 @@ public class ConnectivityTest castLandscape = (AwsLandscape) landscape; final CreateDNSBasedLoadBalancerMapping.Builder> builder = CreateDNSBasedLoadBalancerMapping.builder(); builder @@ -176,7 +228,7 @@ public class ConnectivityTest { /** * Obtains the Sailing Analytics processes running on this host. Can be zero or more. * @param optionalTimeout TODO + * @param privateKeyEncryptionPassphrase TODO */ - Iterable getApplicationProcesses(Optional optionalTimeout) throws SftpException, JSchException, IOException, InterruptedException; + Iterable getApplicationProcesses(Optional optionalTimeout, byte[] privateKeyEncryptionPassphrase) throws SftpException, JSchException, IOException, InterruptedException; AwsLandscape getLandscape(); } diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/AwsLandscape.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/AwsLandscape.java index 7efa5931b0e..515daa827a9 100755 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/AwsLandscape.java +++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/AwsLandscape.java @@ -221,24 +221,33 @@ extends Landscape { /** * Uploads the public key to AWS under the name "keyName", stores it in this landscape and returns the key pair ID */ - String importKeyPair(Region region, byte[] publicKey, byte[] unencryptedPrivateKey, String keyName) throws JSchException; + String importKeyPair(Region region, byte[] publicKey, byte[] encryptedPrivateKey, String keyName) throws JSchException; void terminate(AwsInstance host); SSHKeyPair getSSHKeyPair(Region region, String keyName); - byte[] getDecryptedPrivateKey(SSHKeyPair keyPair) throws JSchException; + /** + * Assumes that the {@code keyPair}'s private key has been encrypted using this landscape's default encryption passphrase + * and uses that to decrypt it. + */ + byte[] getDecryptedPrivateKey(SSHKeyPair keyPair, byte[] privateKeyEncryptionPassphrase) throws JSchException; - void addSSHKeyPair(com.sap.sse.landscape.Region region, String creator, String keyName, KeyPair keyPairWithDecryptedPrivateKey); + /** + * Adds a key pair with {@link KeyPair#decrypt(byte[]) decrypted} private key to the AWS {@code region} identified + * and stores it persistently also in the local server's database with the private key encrypted. + */ + void addSSHKeyPair(com.sap.sse.landscape.Region region, String creator, String keyName, KeyPair keyPairWithDecryptedPrivateKey) throws JSchException; /** * Creates a key pair with the given name in the region specified and obtains the key details and stores them in * this landscape persistently, such that {@link #getKeyPairInfo(Region, String)} as well as - * {@link #getSSHKeyPair(Region, String)} will be able to obtain (information on) the key. + * {@link #getSSHKeyPair(Region, String)} will be able to obtain (information on) the key. The private key is + * stored encrypted with the passphrase provided as parameter {@code privateKeyEncryptionPassphrase}. * * @return the key ID as string, usually starting with the prefix "key-" */ - SSHKeyPair createKeyPair(Region region, String keyName) throws JSchException; + SSHKeyPair createKeyPair(Region region, String keyName, byte[] privateKeyEncryptionPassphrase) throws JSchException; Instance getInstance(String instanceId, Region region); @@ -487,19 +496,19 @@ extends Landscape { /** * Obtains all {@link #getApplicationProcessHostsByTag(Region, String, BiFunction) hosts} with a tag whose key is * specified by {@code tagName} and discovers all application server processes configured on it. These are then - * grouped by {@link ApplicationProcess#getServerName(Optional) server name}, and using + * grouped by {@link ApplicationProcess#getServerName(Optional, byte[]) server name}, and using * {@link ApplicationProcess#getMasterServerName(Optional)} the master/replica relationships between the processes with equal server * name are discovered. From this, an {@link ApplicationReplicaSet} is established per server name. - * * @param processFactoryFromHostAndServerDirectory * takes the host and the server directory as arguments and is expected to produce an * {@link ApplicationProcess} object of some sort. * @param optionalTimeout * an optional timeout for communicating with the application server(s) to try to read the application * configuration; used, e.g., as timeout during establishing SSH connections + * @param privateKeyEncryptionPassphrase TODO */ Iterable> getApplicationReplicaSetsByTag(Region region, String tagName, BiFunction processFactoryFromHostAndServerDirectory, - Optional optionalTimeout) throws Exception; + Optional optionalTimeout, byte[] privateKeyEncryptionPassphrase) throws Exception; } diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/ReverseProxy.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/ReverseProxy.java index 9b957cc1c37..f9048cbd7bc 100755 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/ReverseProxy.java +++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/ReverseProxy.java @@ -52,29 +52,33 @@ public interface ReverseProxy optionalTimeout) throws InterruptedException, JSchException, IOException; + void createInternalStatusRedirect(Optional optionalTimeout, byte[] privateKeyEncryptionPassphrase) throws InterruptedException, JSchException, IOException; /** * Removes any existing redirect mapping for the {@code hostname} provided. If no such mapping * exists, the method does nothing. + * @param privateKeyEncryptionPassphrase TODO */ - void removeRedirect(String hostname) throws InterruptedException, JSchException, IOException; + void removeRedirect(String hostname, byte[] privateKeyEncryptionPassphrase) throws InterruptedException, JSchException, IOException; - void removeRedirect(Scope scope) throws IOException, InterruptedException, JSchException; + void removeRedirect(Scope scope, byte[] privateKeyEncryptionPassphrase) throws IOException, InterruptedException, JSchException; /** * {@link AwsLandscape#terminate(AwsInstance) Terminates} all {@link #getHosts() hosts} that form this reverse diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/ApacheReverseProxy.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/ApacheReverseProxy.java index dd1d8d3e4ba..3568a842686 100755 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/ApacheReverseProxy.java +++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/ApacheReverseProxy.java @@ -80,15 +80,21 @@ implements com.sap.sse.landscape.Process { return hostname+CONFIG_FILE_EXTENSION; } - private void setRedirect(String configFileNameForHostname, String macroName, String hostname, String... macroArguments) throws InterruptedException, JSchException, IOException { - final String command = "echo \"Use "+macroName+" "+hostname+" "+String.join(" ", macroArguments)+"\" >"+getConfigFilePath(configFileNameForHostname)+ - "; service httpd reload"; - logger.info("Standard output from setting up the re-direct for "+hostname+" and reloading the Apache httpd server: "+ - runCommandAndReturnStdoutAndStderr(command, "Standard error from setting up the re-direct for "+hostname+" and reloading the Apache httpd server: ", Level.INFO)); + private void setRedirect(String configFileNameForHostname, String macroName, String hostname, + byte[] privateKeyEncryptionPassphrase, String... macroArguments) + throws InterruptedException, JSchException, IOException { + final String command = "echo \"Use " + macroName + " " + hostname + " " + String.join(" ", macroArguments) + + "\" >" + getConfigFilePath(configFileNameForHostname) + "; service httpd reload"; + logger.info("Standard output from setting up the re-direct for " + hostname + + " and reloading the Apache httpd server: " + + runCommandAndReturnStdoutAndStderr(command, + "Standard error from setting up the re-direct for " + hostname + + " and reloading the Apache httpd server: ", + Level.INFO, privateKeyEncryptionPassphrase)); } - private String runCommandAndReturnStdoutAndStderr(String command, String stderrLogPrefix, Level stderrLogLevel) throws IOException, InterruptedException, JSchException { - final SshCommandChannel sshChannel = getHost().createRootSshChannel(TIMEOUT); + private String runCommandAndReturnStdoutAndStderr(String command, String stderrLogPrefix, Level stderrLogLevel, byte[] privateKeyEncryptionPassphrase) throws IOException, InterruptedException, JSchException { + final SshCommandChannel sshChannel = getHost().createRootSshChannel(TIMEOUT, privateKeyEncryptionPassphrase); final String stdout = sshChannel.runCommandAndReturnStdoutAndLogStderr(command, stderrLogPrefix, stderrLogLevel); return stdout; } @@ -98,60 +104,65 @@ implements com.sap.sse.landscape.Process { } @Override - public void setScopeRedirect(Scope scope,ProcessT applicationReplicaSet) { + public void setScopeRedirect(Scope scope, ProcessT applicationReplicaSet) { // TODO Implement ApacheReverseProxy.setScopeRedirect(...) } @Override - public void setPlainRedirect(String hostname, ProcessT applicationProcess) throws InterruptedException, JSchException, IOException { + public void setPlainRedirect(String hostname, ProcessT applicationProcess, byte[] privateKeyEncryptionPassphrase) throws InterruptedException, JSchException, IOException { final String host = applicationProcess.getHost().getPrivateAddress().getHostAddress(); final int port = applicationProcess.getPort(); - setRedirect(getConfigFileNameForHostname(hostname), PLAIN_REDIRECT_MACRO, hostname, host, ""+port); + setRedirect(getConfigFileNameForHostname(hostname), PLAIN_REDIRECT_MACRO, hostname, privateKeyEncryptionPassphrase, host, ""+port); } @Override - public void setHomeRedirect(String hostname, ProcessT applicationProcess) throws InterruptedException, JSchException, IOException { + public void setHomeRedirect(String hostname, ProcessT applicationProcess, byte[] privateKeyEncryptionPassphrase) throws InterruptedException, JSchException, IOException { final String host = applicationProcess.getHost().getPrivateAddress().getHostAddress(); final int port = applicationProcess.getPort(); - setRedirect(getConfigFileNameForHostname(hostname), HOME_REDIRECT_MACRO, hostname, host, ""+port); + setRedirect(getConfigFileNameForHostname(hostname), HOME_REDIRECT_MACRO, hostname, privateKeyEncryptionPassphrase, host, ""+port); } @Override - public void setEventRedirect(String hostname, ProcessT applicationProcess, UUID eventId) throws InterruptedException, JSchException, IOException { + public void setEventRedirect(String hostname, ProcessT applicationProcess, UUID eventId, byte[] privateKeyEncryptionPassphrase) throws InterruptedException, JSchException, IOException { final String host = applicationProcess.getHost().getPrivateAddress().getHostAddress(); final int port = applicationProcess.getPort(); - setRedirect(getConfigFileNameForHostname(hostname), EVENT_REDIRECT_MACRO, hostname, eventId.toString(), host, ""+port); + setRedirect(getConfigFileNameForHostname(hostname), EVENT_REDIRECT_MACRO, hostname, privateKeyEncryptionPassphrase, eventId.toString(), host, ""+port); } @Override public void setEventSeriesRedirect(String hostname, ProcessT applicationProcess, - UUID leaderboardGroupId) throws InterruptedException, JSchException, IOException { + UUID leaderboardGroupId, byte[] privateKeyEncryptionPassphrase) throws InterruptedException, JSchException, IOException { final String host = applicationProcess.getHost().getPrivateAddress().getHostAddress(); final int port = applicationProcess.getPort(); - setRedirect(getConfigFileNameForHostname(hostname), SERIES_REDIRECT_MACRO, hostname, leaderboardGroupId.toString(), host, ""+port); + setRedirect(getConfigFileNameForHostname(hostname), SERIES_REDIRECT_MACRO, hostname, privateKeyEncryptionPassphrase, leaderboardGroupId.toString(), host, ""+port); } @Override - public void createInternalStatusRedirect(Optional optionalTimeout) throws InterruptedException, JSchException, IOException { - setRedirect(CONFIG_FILE_FOR_INTERNALS, STATUS, getHost().getPublicAddress(optionalTimeout).getCanonicalHostName(), INTERNAL_SERVER_STATUS); + public void createInternalStatusRedirect(Optional optionalTimeout, byte[] privateKeyEncryptionPassphrase) throws InterruptedException, JSchException, IOException { + setRedirect(CONFIG_FILE_FOR_INTERNALS, STATUS, getHost().getPublicAddress(optionalTimeout).getCanonicalHostName(), privateKeyEncryptionPassphrase, INTERNAL_SERVER_STATUS); } @Override - public void removeRedirect(Scope scope) throws IOException, InterruptedException, JSchException { + public void removeRedirect(Scope scope, byte[] privateKeyEncryptionPassphrase) throws IOException, InterruptedException, JSchException { final String configFilePath = getConfigFilePath(getConfigFileNameForScope(scope)); - removeRedirect(configFilePath, scope.toString()); + removeRedirect(configFilePath, scope.toString(), privateKeyEncryptionPassphrase); } @Override - public void removeRedirect(String hostname) throws IOException, InterruptedException, JSchException { + public void removeRedirect(String hostname, byte[] privateKeyEncryptionPassphrase) throws IOException, InterruptedException, JSchException { final String configFilePath = getConfigFilePath(getConfigFileNameForHostname(hostname)); - removeRedirect(configFilePath, hostname); + removeRedirect(configFilePath, hostname, privateKeyEncryptionPassphrase); } - private void removeRedirect(String configFilePath, String redirectNameForLogOutput) throws IOException, InterruptedException, JSchException { - final String command = "rm "+configFilePath+"; service httpd reload"; - logger.info("Standard output from removing the re-direct for "+redirectNameForLogOutput+" and reloading the Apache httpd server: "+ - runCommandAndReturnStdoutAndStderr(command, "Standard error from removing the re-direct for "+redirectNameForLogOutput+" and reloading the Apache httpd server: ", Level.INFO)); + private void removeRedirect(String configFilePath, String redirectNameForLogOutput, + byte[] privateKeyEncryptionPassphrase) throws IOException, InterruptedException, JSchException { + final String command = "rm " + configFilePath + "; service httpd reload"; + logger.info("Standard output from removing the re-direct for " + redirectNameForLogOutput + + " and reloading the Apache httpd server: " + + runCommandAndReturnStdoutAndStderr(command, + "Standard error from removing the re-direct for " + redirectNameForLogOutput + + " and reloading the Apache httpd server: ", + Level.INFO, privateKeyEncryptionPassphrase)); } @Override diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/ApacheReverseProxyCluster.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/ApacheReverseProxyCluster.java index 6041afda118..3d2c71c4d6d 100755 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/ApacheReverseProxyCluster.java +++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/ApacheReverseProxyCluster.java @@ -90,32 +90,32 @@ implements ReverseProxyCluster proxy : getReverseProxies()) { - proxy.setPlainRedirect(hostname, applicationProcess); + proxy.setPlainRedirect(hostname, applicationProcess, privateKeyEncryptionPassphrase); } } @Override - public void setHomeRedirect(String hostname, ProcessT applicationProcess) throws InterruptedException, JSchException, IOException { + public void setHomeRedirect(String hostname, ProcessT applicationProcess, byte[] privateKeyEncryptionPassphrase) throws InterruptedException, JSchException, IOException { for (final ApacheReverseProxy proxy : getReverseProxies()) { - proxy.setHomeRedirect(hostname, applicationProcess); + proxy.setHomeRedirect(hostname, applicationProcess, privateKeyEncryptionPassphrase); } } @Override public void setEventRedirect(String hostname, ProcessT applicationProcess, - UUID eventId) throws InterruptedException, JSchException, IOException { + UUID eventId, byte[] privateKeyEncryptionPassphrase) throws InterruptedException, JSchException, IOException { for (final ApacheReverseProxy proxy : getReverseProxies()) { - proxy.setEventRedirect(hostname, applicationProcess, eventId); + proxy.setEventRedirect(hostname, applicationProcess, eventId, privateKeyEncryptionPassphrase); } } @Override public void setEventSeriesRedirect(String hostname, ProcessT applicationProcess, - UUID leaderboardGroupId) throws InterruptedException, JSchException, IOException { + UUID leaderboardGroupId, byte[] privateKeyEncryptionPassphrase) throws InterruptedException, JSchException, IOException { for (final ApacheReverseProxy proxy : getReverseProxies()) { - proxy.setEventSeriesRedirect(hostname, applicationProcess, leaderboardGroupId); + proxy.setEventSeriesRedirect(hostname, applicationProcess, leaderboardGroupId, privateKeyEncryptionPassphrase); } } @@ -127,23 +127,23 @@ implements ReverseProxyCluster optionalTimeout) throws InterruptedException, JSchException, IOException { + public void createInternalStatusRedirect(Optional optionalTimeout, byte[] privateKeyEncryptionPassphrase) throws InterruptedException, JSchException, IOException { for (final ApacheReverseProxy proxy : getReverseProxies()) { - proxy.createInternalStatusRedirect(optionalTimeout); + proxy.createInternalStatusRedirect(optionalTimeout, privateKeyEncryptionPassphrase); } } @Override - public void removeRedirect(String hostname) throws IOException, InterruptedException, JSchException { + public void removeRedirect(String hostname, byte[] privateKeyEncryptionPassphrase) throws IOException, InterruptedException, JSchException { for (final ApacheReverseProxy proxy : getReverseProxies()) { - proxy.removeRedirect(hostname); + proxy.removeRedirect(hostname, privateKeyEncryptionPassphrase); } } @Override - public void removeRedirect(Scope scope) throws IOException, InterruptedException, JSchException { + public void removeRedirect(Scope scope, byte[] privateKeyEncryptionPassphrase) throws IOException, InterruptedException, JSchException { for (final ApacheReverseProxy proxy : getReverseProxies()) { - proxy.removeRedirect(scope); + proxy.removeRedirect(scope, privateKeyEncryptionPassphrase); } } } diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/ApplicationProcessHostImpl.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/ApplicationProcessHostImpl.java index a836e1da4e7..973d04955bd 100755 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/ApplicationProcessHostImpl.java +++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/ApplicationProcessHostImpl.java @@ -51,13 +51,18 @@ implements ApplicationProcessHost { /** * The implementation scans the {@link ApplicationProcessHost#DEFAULT_SERVERS_PATH application server deployment - * folder} for sub-folders. In those sub-folders, the configuration file is analyzed for the port number to instantiate - * an {@link ApplicationProcess} object for each one. + * folder} for sub-folders. In those sub-folders, the configuration file is analyzed for the port number to + * instantiate an {@link ApplicationProcess} object for each one. + * + * @param privateKeyEncryptionPassphrase + * the pass phrase for the private key that belongs to the instance's public key used for start-up */ @Override - public Iterable getApplicationProcesses(Optional optionalTimeout) throws SftpException, JSchException, IOException, InterruptedException { + public Iterable getApplicationProcesses(Optional optionalTimeout, + byte[] privateKeyEncryptionPassphrase) + throws SftpException, JSchException, IOException, InterruptedException { final Set result = new HashSet<>(); - final ChannelSftp sftpChannel = createRootSftpChannel(optionalTimeout); + final ChannelSftp sftpChannel = createRootSftpChannel(optionalTimeout, privateKeyEncryptionPassphrase); if (optionalTimeout.isPresent()) { sftpChannel.connect((int) optionalTimeout.get().asMillis()); } else { diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AwsInstanceImpl.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AwsInstanceImpl.java index b8ed8ea352d..5cebde8603a 100755 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AwsInstanceImpl.java +++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AwsInstanceImpl.java @@ -120,23 +120,27 @@ public class AwsInstanceImpl optionalTimeout) throws JSchException, IOException, InterruptedException { - return createSshChannel(ROOT_USER_NAME, optionalTimeout); + public SshCommandChannel createRootSshChannel(Optional optionalTimeout, + byte[] privateKeyEncryptionPassphrase) throws JSchException, IOException, InterruptedException { + return createSshChannel(ROOT_USER_NAME, optionalTimeout, privateKeyEncryptionPassphrase); } /** @@ -161,22 +168,28 @@ public class AwsInstanceImpl optionalTimeout) throws JSchException, IOException, InterruptedException { - return new SshCommandChannelImpl((ChannelExec) createSshChannelInternal(sshUserName, "exec", optionalTimeout)); + public SshCommandChannel createSshChannel(String sshUserName, Optional optionalTimeout, + byte[] privateKeyEncryptionPassphrase) throws JSchException, IOException, InterruptedException { + return new SshCommandChannelImpl((ChannelExec) createSshChannelInternal(sshUserName, "exec", optionalTimeout, privateKeyEncryptionPassphrase)); } - private Channel createSshChannelInternal(String sshUserName, String channelType, Optional optionalTimeout) throws JSchException, IOException, InterruptedException { + private Channel createSshChannelInternal(String sshUserName, String channelType, Optional optionalTimeout, + byte[] privateKeyEncryptionPassphrase) throws JSchException, IOException, InterruptedException { final TimePoint start = TimePoint.now(); - logger.info("Creating SSH "+channelType+" channel for SSH user "+sshUserName+ + logger.info( + "Creating SSH "+channelType+" channel for SSH user "+sshUserName+ " to instance with ID "+getInstanceId()); Channel channel = null; while (channel == null && (!optionalTimeout.isPresent() || start.until(TimePoint.now()).compareTo(optionalTimeout.get()) < 0)) { Session session = null; try { - session = createSshSession(sshUserName); + session = createSshSession(sshUserName, privateKeyEncryptionPassphrase); session.setUserInfo(new YesUserInfo()); session.connect(optionalTimeout.map(d->d.asMillis()).orElse(0l).intValue()); channel = session.openChannel(channelType); @@ -200,13 +213,16 @@ public class AwsInstanceImpl optionalTimeout) throws JSchException, IOException, InterruptedException { - return (ChannelSftp) createSshChannelInternal(sshUserName, "sftp", optionalTimeout); + public ChannelSftp createSftpChannel(String sshUserName, Optional optionalTimeout, + byte[] privateKeyEncryptionPassphrase) throws JSchException, IOException, InterruptedException { + return (ChannelSftp) createSshChannelInternal(sshUserName, "sftp", optionalTimeout, + privateKeyEncryptionPassphrase); } @Override - public ChannelSftp createRootSftpChannel(Optional optionalTimeout) throws JSchException, IOException, InterruptedException { - return createSftpChannel(ROOT_USER_NAME, optionalTimeout); + public ChannelSftp createRootSftpChannel(Optional optionalTimeout, byte[] privateKeyEncryptionPassphrase) + throws JSchException, IOException, InterruptedException { + return createSftpChannel(ROOT_USER_NAME, optionalTimeout, privateKeyEncryptionPassphrase); } @Override diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AwsLandscapeImpl.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AwsLandscapeImpl.java index c20e6febe66..f638835ea20 100755 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AwsLandscapeImpl.java +++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AwsLandscapeImpl.java @@ -157,12 +157,6 @@ implements AwsLandscape { private ConcurrentMap, SSHKeyPair> sshKeyPairs; private final AwsRegion globalRegion; - /** - * Used for the symmetric encryption / decryption of private SSH keys. See also - * {@link #getDecryptedPrivateKey(SSHKeyPair)}. - */ - private final byte[] privateKeyEncryptionPassphrase; - public AwsLandscapeImpl() { this(System.getProperty(ACCESS_KEY_ID_SYSTEM_PROPERTY_NAME), System.getProperty(SECRET_ACCESS_KEY_SYSTEM_PROPERTY_NAME)); @@ -177,7 +171,6 @@ implements AwsLandscape { public AwsLandscapeImpl(String accessKeyId, String secretAccessKey, DomainObjectFactory domainObjectFactory, MongoObjectFactory mongoObjectFactory) { - this.privateKeyEncryptionPassphrase = ("aw4raif87l"+"098sf;;50").getBytes(); this.accessKeyId = accessKeyId; this.secretAccessKey = secretAccessKey; this.globalRegion = new AwsRegion(Region.AWS_GLOBAL); @@ -203,7 +196,8 @@ implements AwsLandscape { } @Override - public void addSSHKeyPair(com.sap.sse.landscape.Region region, String creator, String keyName, KeyPair keyPairWithDecryptedPrivateKey) { + public void addSSHKeyPair(com.sap.sse.landscape.Region region, String creator, String keyName, KeyPair keyPairWithDecryptedPrivateKey) throws JSchException { + assert !keyPairWithDecryptedPrivateKey.isEncrypted(); addSSHKeyPair(new SSHKeyPair(region.getId(), creator, TimePoint.now(), keyName, keyPairWithDecryptedPrivateKey.getPublicKeyBlob(), getPrivateKeyBytes(keyPairWithDecryptedPrivateKey))); } @@ -214,7 +208,7 @@ implements AwsLandscape { } @Override - public SSHKeyPair createKeyPair(com.sap.sse.landscape.Region region, String keyName) throws JSchException { + public SSHKeyPair createKeyPair(com.sap.sse.landscape.Region region, String keyName, byte[] privateKeyEncryptionPassphrase) throws JSchException { final CreateKeyPairResponse keyPairResponse = getEc2Client(getRegion(region)) .createKeyPair(CreateKeyPairRequest.builder().keyName(keyName).build()); final String keyMaterial = keyPairResponse.keyMaterial(); @@ -609,7 +603,7 @@ implements AwsLandscape { } @Override - public String importKeyPair(com.sap.sse.landscape.Region region, byte[] publicKey, byte[] unencryptedPrivateKey, String keyName) throws JSchException { + public String importKeyPair(com.sap.sse.landscape.Region region, byte[] publicKey, byte[] encryptedPrivateKey, String keyName) throws JSchException { final String keyId = getEc2Client(getRegion(region)).importKeyPair(ImportKeyPairRequest.builder().keyName(keyName) .publicKeyMaterial(SdkBytes.fromByteArray(publicKey)).build()).keyPairId(); Object principal; @@ -620,7 +614,7 @@ implements AwsLandscape { principal = null; } final SSHKeyPair keyPair = new SSHKeyPair(region.getId(), principal==null?"":principal.toString(), - TimePoint.now(), keyName, publicKey, unencryptedPrivateKey, privateKeyEncryptionPassphrase); + TimePoint.now(), keyName, publicKey, encryptedPrivateKey); addSSHKeyPair(keyPair); return keyId; } @@ -631,7 +625,7 @@ implements AwsLandscape { } @Override - public byte[] getDecryptedPrivateKey(SSHKeyPair keyPair) throws JSchException { + public byte[] getDecryptedPrivateKey(SSHKeyPair keyPair, byte[] privateKeyEncryptionPassphrase) throws JSchException { final ByteArrayOutputStream bos = new ByteArrayOutputStream(); keyPair.getKeyPair(new JSch(), privateKeyEncryptionPassphrase).writePrivateKey(bos); return bos.toByteArray(); @@ -1033,13 +1027,13 @@ implements AwsLandscape { @Override public Iterable> getApplicationReplicaSetsByTag(com.sap.sse.landscape.Region region, String tagName, - BiFunction processFactoryFromHostAndServerDirectory, Optional optionalTimeout) throws Exception { + BiFunction processFactoryFromHostAndServerDirectory, Optional optionalTimeout, byte[] privateKeyEncryptionPassphrase) throws Exception { final Iterable> hosts = getApplicationProcessHostsByTag(region, tagName, processFactoryFromHostAndServerDirectory); final Map mastersByServerName = new HashMap<>(); final Map> replicasByServerName = new HashMap<>(); for (final ApplicationProcessHost host : hosts) { - for (final ProcessT applicationProcess : host.getApplicationProcesses(optionalTimeout)) { - final String serverName = applicationProcess.getServerName(optionalTimeout); + for (final ProcessT applicationProcess : host.getApplicationProcesses(optionalTimeout, privateKeyEncryptionPassphrase)) { + final String serverName = applicationProcess.getServerName(optionalTimeout, privateKeyEncryptionPassphrase); final String masterServerName = applicationProcess.getMasterServerName(optionalTimeout); if (masterServerName != null && Util.equalsWithNull(masterServerName, serverName)) { // then applicationProcess is a replica in the serverName cluster: diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/orchestration/CreateLoadBalancerMapping.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/orchestration/CreateLoadBalancerMapping.java index fbe57141f31..b35eb6c8247 100755 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/orchestration/CreateLoadBalancerMapping.java +++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/orchestration/CreateLoadBalancerMapping.java @@ -101,6 +101,7 @@ extends ProcedureWithTargetGroup { BuilderT setProcess(ProcessT process); BuilderT setHostname(String hostname); BuilderT setTimeout(Duration timeout); + BuilderT setPrivateKeyEncryptionPassphrase(byte[] privateKeyEncryptionPassphrase); } protected abstract static class BuilderImpl, @@ -113,6 +114,7 @@ extends ProcedureWithTargetGroup { private String hostname; private ProcessT process; private Optional optionalTimeout = Optional.empty(); + private byte[] privateKeyEncryptionPassphrase; @Override public BuilderT setProcess(ProcessT process) { @@ -143,6 +145,12 @@ extends ProcedureWithTargetGroup { protected Optional getOptionalTimeout() { return optionalTimeout; } + + @Override + public BuilderT setPrivateKeyEncryptionPassphrase(byte[] privateKeyEncryptionPassphrase) { + this.privateKeyEncryptionPassphrase = privateKeyEncryptionPassphrase; + return self(); + } @Override protected String getServerName() throws Exception { @@ -150,7 +158,7 @@ extends ProcedureWithTargetGroup { if (super.getServerName() != null) { result = super.getServerName(); } else { - result = getProcess().getServerName(getOptionalTimeout()); + result = getProcess().getServerName(getOptionalTimeout(), privateKeyEncryptionPassphrase); } return result; } diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/orchestration/StartAwsHost.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/orchestration/StartAwsHost.java index fabdff30cb0..b390df2508c 100755 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/orchestration/StartAwsHost.java +++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/orchestration/StartAwsHost.java @@ -16,6 +16,7 @@ import com.jcraft.jsch.JSchException; import com.sap.sse.common.Duration; import com.sap.sse.common.Util; import com.sap.sse.landscape.AvailabilityZone; +import com.sap.sse.landscape.Host; import com.sap.sse.landscape.Landscape; import com.sap.sse.landscape.ProcessConfigurationVariable; import com.sap.sse.landscape.Region; @@ -56,6 +57,7 @@ extends StartHost { private final Iterable securityGroups; private final Optional tags; private final HostSupplier hostSupplier; + private final byte[] privateKeyEncryptionPassphrase; private HostT host; /** @@ -89,7 +91,19 @@ extends StartHost { BuilderT setAvailabilityZone(AwsAvailabilityZone availabilityZone); + /** + * Tells the name of the SSH key stored in the AWS landscape that is to be used to launch the instance; the + * respective public key will be added so that an owner of the corresponding private key can + * {@link Host#createSshChannel(String, Optional, byte[]) SSH} into the resulting host.

+ * + * Make sure to also provide the {@link #setPrivateKeyEncryptionPassphrase(byte[]) private key pass phrase} + * that is required to decrypt the pass phrase of the private key belonging to the public key identified + * by {@code keyName}. This private key is expected to be stored together with the entire key pair in the + * "landscape" persistently. + */ BuilderT setKeyName(String keyName); + + BuilderT setPrivateKeyEncryptionPassphrase(byte[] privateKeyEncryptionPassphrase); BuilderT setSecurityGroups(Iterable securityGroups); @@ -120,6 +134,7 @@ extends StartHost { private AwsRegion region; private String instanceName; private HostSupplier hostSupplier; + private byte[] privateKeyEncryptionPassphrase; protected AwsLandscape getLandscape() { return (AwsLandscape) super.getLandscape(); @@ -233,6 +248,16 @@ extends StartHost { this.hostSupplier = hostSupplier; return self(); } + + @Override + public BuilderT setPrivateKeyEncryptionPassphrase(byte[] privateKeyEncryptionPassphrase) { + this.privateKeyEncryptionPassphrase = privateKeyEncryptionPassphrase; + return self(); + } + + protected byte[] getPrivateKeyEncryptionPassphrase() { + return privateKeyEncryptionPassphrase; + } } protected StartAwsHost(BuilderImpl, ShardingKey, MetricsT, ProcessT, HostT> builder) { @@ -247,6 +272,7 @@ extends StartHost { this.tags = Optional.of(builder.getTags().orElse(Tags.empty()).and(NAME_TAG_NAME, builder.getInstanceName())); this.securityGroups = builder.getSecurityGroups(); this.hostSupplier = builder.getHostSupplier(); + this.privateKeyEncryptionPassphrase = builder.getPrivateKeyEncryptionPassphrase(); } protected static { } } } + + protected byte[] getPrivateKeyEncryptionPassphrase() { + return privateKeyEncryptionPassphrase; + } protected void copyRootAuthorizedKeysToOtherUser(String username, Optional optionalTimeout) throws JSchException, IOException, InterruptedException { - final SshCommandChannel sshChannel = getHost().createRootSshChannel(optionalTimeout); + final SshCommandChannel sshChannel = getHost().createRootSshChannel(optionalTimeout, getPrivateKeyEncryptionPassphrase()); final String sailingUserSsh = "/home/" + username + "/.ssh"; final String sailingUserAuthorizedKeys = sailingUserSsh + "/authorized_keys"; logger.info("Appended root's authorized_keys also to " + username + "'s authorized_keys. stdout: " diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/orchestration/StartMongoDBServer.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/orchestration/StartMongoDBServer.java index 71d60bb4889..d431d1d1643 100755 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/orchestration/StartMongoDBServer.java +++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/orchestration/StartMongoDBServer.java @@ -259,6 +259,4 @@ extends StartAwsHost optionalTimeout) throws JSchException, IOException, InterruptedException; + SshCommandChannel createSshChannel(String sshUserName, Optional optionalTimeout, byte[] privateKeyEncryptionPassphrase) throws JSchException, IOException, InterruptedException; /** * Connects to an SSH session for the "root" user with a "shell" channel * - * @see #createSshChannel(String, Optional) + * @param privateKeyEncryptionPassphrase + * the pass phrase for the private key that belongs to the instance's public key used for start-up + * + * @see #createSshChannel(String, Optional, byte[]) */ - SshCommandChannel createRootSshChannel(Optional optionalTimeout) throws JSchException, IOException, InterruptedException; - - ChannelSftp createSftpChannel(String sshUserName, Optional optionalTimeout) throws JSchException, IOException, InterruptedException; + SshCommandChannel createRootSshChannel(Optional optionalTimeout, byte[] privateKeyEncryptionPassphrase) throws JSchException, IOException, InterruptedException; - ChannelSftp createRootSftpChannel(Optional optionalTimeout) throws JSchException, IOException, InterruptedException; + /** + * @param privateKeyEncryptionPassphrase + * the pass phrase for the private key that belongs to the instance's public key used for start-up + */ + ChannelSftp createSftpChannel(String sshUserName, Optional optionalTimeout, byte[] privateKeyEncryptionPassphrase) throws JSchException, IOException, InterruptedException; + + /** + * @param privateKeyEncryptionPassphrase + * the pass phrase for the private key that belongs to the instance's public key used for start-up + */ + ChannelSftp createRootSftpChannel(Optional optionalTimeout, byte[] privateKeyEncryptionPassphrase) throws JSchException, IOException, InterruptedException; /** * Tells where in the cloud this host runs; the availability zone {@link AvailabilityZone#getRegion() implies} the diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/application/ApplicationProcess.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/application/ApplicationProcess.java index 7b22973c467..520c41b206b 100755 --- a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/application/ApplicationProcess.java +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/application/ApplicationProcess.java @@ -29,9 +29,10 @@ extends Process { * @param releaseRepository * mandatory parameter required to enable resolving the release and enabling the download of its * artifacts, including its release notes + * @param privateKeyEncryptionPassphrase TODO * @return the release that this process is currently running */ - Release getRelease(ReleaseRepository releaseRepository, Optional optionalTimeout) throws JSchException, IOException, SftpException, InterruptedException; + Release getRelease(ReleaseRepository releaseRepository, Optional optionalTimeout, byte[] privateKeyEncryptionPassphrase) throws JSchException, IOException, SftpException, InterruptedException; /** * Tries to shut down an OSGi application server process cleanly by sending the "shutdown" OSGi command to this @@ -48,7 +49,7 @@ extends Process { */ boolean tryCleanShutdown(Duration timeout, boolean forceAfterTimeout); - int getTelnetPortToOSGiConsole(Optional optionalTimeout) throws NumberFormatException, JSchException, IOException, SftpException, InterruptedException; + int getTelnetPortToOSGiConsole(Optional optionalTimeout, byte[] privateKeyEncryptionPassphrase) throws NumberFormatException, JSchException, IOException, SftpException, InterruptedException; /** * @return the directory as an absolute path that can be used, e.g., in a {@link ChannelSftp} to change directory to @@ -62,10 +63,11 @@ extends Process { * as the base name of the {@link #getServerDirectory() server's directory}. Often, it is also used as the name of * the {@link Database}, at least when this is a master node, and the name of the RabbitMQ fan-out exchange used * for replication. + * @param privateKeyEncryptionPassphrase TODO */ - String getServerName(Optional optionalTimeout) throws Exception; + String getServerName(Optional optionalTimeout, byte[] privateKeyEncryptionPassphrase) throws Exception; - String getEnvSh(Optional optionalTimeout) throws JSchException, IOException, SftpException, InterruptedException; + String getEnvSh(Optional optionalTimeout, byte[] privateKeyEncryptionPassphrase) throws JSchException, IOException, SftpException, InterruptedException; /** * The URL path (everything following the hostname and starting with "/" but without any fragment) that can be @@ -80,15 +82,17 @@ extends Process { /** * Obtains the last definition of the process configuration variable specified, or {@code null} if that variable cannot be found * in the evaluated {@code env.sh} file. + * @param privateKeyEncryptionPassphrase TODO */ - String getEnvShValueFor(ProcessConfigurationVariable variable, Optional optionalTimeout) + String getEnvShValueFor(ProcessConfigurationVariable variable, Optional optionalTimeout, byte[] privateKeyEncryptionPassphrase) throws JSchException, IOException, InterruptedException; /** * Obtains the last definition of the process configuration variable specified, or {@code null} if that variable isn't set * by evaluating the {@code env.sh} file on the {@link #getHost() host}. + * @param privateKeyEncryptionPassphrase TODO */ - String getEnvShValueFor(String variableName, Optional optionalTimeout) + String getEnvShValueFor(String variableName, Optional optionalTimeout, byte[] privateKeyEncryptionPassphrase) throws JSchException, IOException, InterruptedException; /** diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/application/impl/ApplicationProcessImpl.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/application/impl/ApplicationProcessImpl.java index 82a5000135d..b2b0441c2c2 100755 --- a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/application/impl/ApplicationProcessImpl.java +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/application/impl/ApplicationProcessImpl.java @@ -45,16 +45,19 @@ implements ApplicationProcess { /** * Absolute path in the file system of the host on which this process is running and that represents * this process's working directory. This directory is expected to contain a file named {@link #ENV_SH} - * whose contents can be obtained using the {@link #getEnvSh(Optional)} method. + * whose contents can be obtained using the {@link #getEnvSh(Optional, byte[])} method. */ private final String serverDirectory; /** - * Alternative constructor that doesn't take the port number as argument but instead tries to obtain it from the {@link #ENV_SH env.sh} file - * located on the {@code host} in the {@code serverDirectory} specified. + * Alternative constructor that doesn't take the port number as argument but instead tries to obtain it from the + * {@link #ENV_SH env.sh} file located on the {@code host} in the {@code serverDirectory} specified. + * + * @param privateKeyEncryptionPassphrase + * the pass phrase for the private key that belongs to the instance's public key used for start-up */ - public ApplicationProcessImpl(Host host, String serverDirectory, Optional optionalTimeout) throws NumberFormatException, JSchException, IOException, InterruptedException { - this(readPortFromDirectory(host, serverDirectory, optionalTimeout), host, serverDirectory); + public ApplicationProcessImpl(Host host, String serverDirectory, Optional optionalTimeout, byte[] privateKeyEncryptionPassphrase) throws NumberFormatException, JSchException, IOException, InterruptedException { + this(readPortFromDirectory(host, serverDirectory, optionalTimeout, privateKeyEncryptionPassphrase), host, serverDirectory); } public ApplicationProcessImpl(int port, Host host, String serverDirectory) { @@ -62,14 +65,20 @@ implements ApplicationProcess { this.serverDirectory = serverDirectory; } - private static int readPortFromDirectory(Host host, String serverDirectory, Optional optionalTimeout) throws NumberFormatException, JSchException, IOException, InterruptedException { - return Integer.parseInt(getEnvShValueFor(host, serverDirectory, DefaultProcessConfigurationVariables.SERVER_PORT.name(), optionalTimeout)); + private static int readPortFromDirectory(Host host, String serverDirectory, Optional optionalTimeout, + byte[] privateKeyEncryptionPassphrase) + throws NumberFormatException, JSchException, IOException, InterruptedException { + return Integer.parseInt( + getEnvShValueFor(host, serverDirectory, DefaultProcessConfigurationVariables.SERVER_PORT.name(), + optionalTimeout, privateKeyEncryptionPassphrase)); } @Override - public Release getRelease(ReleaseRepository releaseRepository, Optional optionalTimeout) throws JSchException, IOException, SftpException, InterruptedException { + public Release getRelease(ReleaseRepository releaseRepository, Optional optionalTimeout, + byte[] privateKeyEncryptionPassphrase) + throws JSchException, IOException, SftpException, InterruptedException { final Pattern pattern = Pattern.compile("^[^-]*-([^ ]*) System:"); - final Matcher matcher = pattern.matcher(getVersionTxt(optionalTimeout)); + final Matcher matcher = pattern.matcher(getVersionTxt(optionalTimeout, privateKeyEncryptionPassphrase)); final Release result; if (matcher.find()) { result = new ReleaseImpl(matcher.group(1), releaseRepository); @@ -80,14 +89,18 @@ implements ApplicationProcess { } /** - * Under the path {@code configuration/jetty/version.txt} each release is expected to present a version string in the - * format {commitId}-{releaseName} System: {mongoDbUriWithoutSchemaOrHostPortDBName}-{expeditionPort}-{replicationHost}:{replicationPort}/{replicationChannel} Started: {yyyyMMddHHmm}. - * With this it is possible to infer the release that will be run upon the next process start, which is also the - * one running now if this process is currently running and no other release has been deployed since the process - * has started. + * Under the path {@code configuration/jetty/version.txt} each release is expected to present a version string in + * the format + * {commitId}-{releaseName} System: {mongoDbUriWithoutSchemaOrHostPortDBName}-{expeditionPort}-{replicationHost}:{replicationPort}/{replicationChannel} Started: {yyyyMMddHHmm}. + * With this it is possible to infer the release that will be run upon the next process start, which is also the one + * running now if this process is currently running and no other release has been deployed since the process has + * started. + * + * @param privateKeyEncryptionPassphrase + * the pass phrase for the private key that belongs to the instance's public key used for start-up */ - private String getVersionTxt(Optional optionalTimeout) throws JSchException, IOException, SftpException, InterruptedException { - return getFileContents(getServerDirectory()+"/"+VERSION_TXT, optionalTimeout); + private String getVersionTxt(Optional optionalTimeout, byte[] privateKeyEncryptionPassphrase) throws JSchException, IOException, SftpException, InterruptedException { + return getFileContents(getServerDirectory()+"/"+VERSION_TXT, optionalTimeout, privateKeyEncryptionPassphrase); } @Override @@ -97,8 +110,10 @@ implements ApplicationProcess { } @Override - public int getTelnetPortToOSGiConsole(Optional optionalTimeout) throws NumberFormatException, JSchException, IOException, SftpException, InterruptedException { - return Integer.parseInt(getEnvShValueFor(DefaultProcessConfigurationVariables.TELNET_PORT, optionalTimeout)); + public int getTelnetPortToOSGiConsole(Optional optionalTimeout, byte[] privateKeyEncryptionPassphrase) + throws NumberFormatException, JSchException, IOException, SftpException, InterruptedException { + return Integer.parseInt(getEnvShValueFor(DefaultProcessConfigurationVariables.TELNET_PORT, optionalTimeout, + privateKeyEncryptionPassphrase)); } /** @@ -106,12 +121,15 @@ implements ApplicationProcess { * by evaluating the {@code env.sh} file on the {@link #getHost() host}. */ @Override - public String getEnvShValueFor(String variableName, Optional optionalTimeout) throws JSchException, IOException, InterruptedException { - return getEnvShValueFor(getHost(), getServerDirectory(), variableName, optionalTimeout); + public String getEnvShValueFor(String variableName, Optional optionalTimeout, + byte[] privateKeyEncryptionPassphrase) throws JSchException, IOException, InterruptedException { + return getEnvShValueFor(getHost(), getServerDirectory(), variableName, optionalTimeout, privateKeyEncryptionPassphrase); } - protected static String getEnvShValueFor(Host host, String serverDirectory, String variableName, Optional optionalTimeout) throws JSchException, IOException, InterruptedException { - final SshCommandChannel sshChannel = host.createRootSshChannel(optionalTimeout); + protected static String getEnvShValueFor(Host host, String serverDirectory, String variableName, + Optional optionalTimeout, byte[] privateKeyEncryptionPassphrase) + throws JSchException, IOException, InterruptedException { + final SshCommandChannel sshChannel = host.createRootSshChannel(optionalTimeout, privateKeyEncryptionPassphrase); final String variableValue = sshChannel.runCommandAndReturnStdoutAndLogStderr(". "+getEnvShPath(serverDirectory)+">/dev/null 2>/dev/null; "+ "echo \"${"+variableName+"}\"", /* stderr prefix */ null, /* stderr log level */ null); return variableValue.endsWith("\n") ? variableValue.substring(0, variableValue.length()-1) : variableValue; @@ -122,8 +140,8 @@ implements ApplicationProcess { * in the evaluated {@code env.sh} file. */ @Override - public String getEnvShValueFor(ProcessConfigurationVariable variable, Optional optionalTimeout) throws JSchException, IOException, InterruptedException { - return getEnvShValueFor(variable.name(), optionalTimeout); + public String getEnvShValueFor(ProcessConfigurationVariable variable, Optional optionalTimeout, byte[] privateKeyEncryptionPassphrase) throws JSchException, IOException, InterruptedException { + return getEnvShValueFor(variable.name(), optionalTimeout, privateKeyEncryptionPassphrase); } @Override @@ -140,18 +158,18 @@ implements ApplicationProcess { } @Override - public String getServerName(Optional optionalTimeout) throws Exception { - return getEnvShValueFor(DefaultProcessConfigurationVariables.SERVER_NAME, optionalTimeout); + public String getServerName(Optional optionalTimeout, byte[] privateKeyEncryptionPassphrase) throws Exception { + return getEnvShValueFor(DefaultProcessConfigurationVariables.SERVER_NAME, optionalTimeout, privateKeyEncryptionPassphrase); } @Override - public String getEnvSh(Optional optionalTimeout) throws JSchException, IOException, SftpException, InterruptedException { - return getFileContents(getEnvShPath(), optionalTimeout); + public String getEnvSh(Optional optionalTimeout, byte[] privateKeyEncryptionPassphrase) throws JSchException, IOException, SftpException, InterruptedException { + return getFileContents(getEnvShPath(), optionalTimeout, privateKeyEncryptionPassphrase); } - protected String getFileContents(String path, Optional optionalTimeout) + protected String getFileContents(String path, Optional optionalTimeout, byte[] privateKeyEncryptionPassphrase) throws JSchException, IOException, SftpException, InterruptedException { - final ChannelSftp sftpChannel = getHost().createRootSftpChannel(optionalTimeout); + final ChannelSftp sftpChannel = getHost().createRootSftpChannel(optionalTimeout, privateKeyEncryptionPassphrase); final ByteArrayOutputStream bos = new ByteArrayOutputStream(); sftpChannel.connect((int) optionalTimeout.orElse(Duration.NULL).asMillis()); sftpChannel.get(path, bos); diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/ssh/SSHKeyPair.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/ssh/SSHKeyPair.java index accd8043a88..319b5d205a8 100755 --- a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/ssh/SSHKeyPair.java +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/ssh/SSHKeyPair.java @@ -8,6 +8,7 @@ import com.jcraft.jsch.KeyPair; import com.sap.sse.common.Named; import com.sap.sse.common.TimePoint; import com.sap.sse.common.impl.NamedImpl; +import com.sap.sse.landscape.Host; /** * Region ID and key name together form a unique key for AWS key pairs. The private key is always stored in an encrypted form, @@ -55,13 +56,19 @@ public class SSHKeyPair extends NamedImpl implements Named { } /** + * Returns a {@link KeyPair} with decrypted private key that can be used to initiate an SSH session, e.g., using + * {@link Host#createSshChannel(String, java.util.Optional, byte[])}. + * * @param passphrase * used to {@link KeyPair#decrypt(byte[]) decrypt} the encrypted private key so that the resulting key - * pair can be used to initiate sessions. + * pair can be used to initiate sessions. Has to equal the passphrase used when encrypting the private key, + * e.g., when calling {@link #SSHKeyPair(String, String, TimePoint, String, byte[], byte[], byte[])}. */ public KeyPair getKeyPair(JSch jsch, byte[] passphrase) throws JSchException { final KeyPair result = KeyPair.load(jsch, getEncryptedPrivateKey(), getPublicKey()); - result.decrypt(passphrase); + if (!result.decrypt(passphrase)) { + throw new IllegalStateException("Could not decrypt private key of "+this+"; probably incorrect passphrase?"); + } return result; }