/*
* Copyright 2011-2014 Amazon Technologies, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at:
*
* http://aws.amazon.com/apache2.0
*
* This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
* OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and
* limitations under the License.
*/
package com.amazonaws.eclipse.rds;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.datatools.connectivity.ConnectionProfileException;
import org.eclipse.datatools.connectivity.IConnectionProfile;
import org.eclipse.datatools.connectivity.ProfileManager;
import org.eclipse.datatools.connectivity.drivers.DriverInstance;
import org.eclipse.datatools.connectivity.drivers.DriverManager;
import org.eclipse.datatools.connectivity.drivers.IDriverMgmtConstants;
import org.eclipse.datatools.connectivity.drivers.IPropertySet;
import org.eclipse.datatools.connectivity.drivers.PropertySetImpl;
import org.eclipse.datatools.connectivity.drivers.jdbc.IJDBCConnectionProfileConstants;
import org.eclipse.datatools.connectivity.drivers.jdbc.IJDBCDriverDefinitionConstants;
import org.eclipse.jface.operation.IRunnableWithProgress;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.eclipse.core.AwsToolkitCore;
import com.amazonaws.eclipse.core.regions.RegionUtils;
import com.amazonaws.eclipse.rds.connectionfactories.DatabaseConnectionFactory;
import com.amazonaws.services.ec2.AmazonEC2;
import com.amazonaws.services.ec2.model.AuthorizeSecurityGroupIngressRequest;
import com.amazonaws.services.ec2.model.IpPermission;
import com.amazonaws.services.rds.AmazonRDS;
import com.amazonaws.services.rds.model.AuthorizationAlreadyExistsException;
import com.amazonaws.services.rds.model.AuthorizeDBSecurityGroupIngressRequest;
import com.amazonaws.services.rds.model.CreateDBSecurityGroupRequest;
import com.amazonaws.services.rds.model.DBInstance;
import com.amazonaws.services.rds.model.DBSecurityGroupMembership;
import com.amazonaws.services.rds.model.DBSecurityGroupNotFoundException;
import com.amazonaws.services.rds.model.DescribeDBSecurityGroupsRequest;
import com.amazonaws.services.rds.model.ModifyDBInstanceRequest;
import com.amazonaws.services.rds.model.VpcSecurityGroupMembership;
class ConfigureRDSDBConnectionRunnable implements IRunnableWithProgress {
private static final String DEFAULT_SECURITY_GROUP_DESCRIPTION = "Security group for remote client tools";
private static final String DEFAULT_SECURITY_GROUP_NAME = "Remote Tool Access Group";
private final AmazonRDS rds;
private final ImportDBInstanceDataModel wizardDataModel;
private DatabaseConnectionFactory connectionFactory;
private boolean completedSuccessfully = false;
public ConfigureRDSDBConnectionRunnable(ImportDBInstanceDataModel wizardDataModel) {
this.wizardDataModel = wizardDataModel;
this.rds = AwsToolkitCore.getClientFactory().getRDSClient();
this.connectionFactory = DatabaseConnectionFactory.createConnectionFactory(wizardDataModel);
}
public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
monitor.beginTask("Configuring database connection", 10);
try {
configureSecurityGroupPermissions(monitor);
DriverInstance driverInstance = getDriver();
/*
* If we aren't able to connect, we should warn users, and let them know some of the possible
* reasons, such as an incorrect password, or if they're connected through a firewall (like our VPN),
* and how to run their DB instance on other ports so corporate firewalls don't cause problems.
*/
IConnectionProfile connectionProfile = createConnectionProfile(driverInstance, new SubProgressMonitor(monitor, 4));
monitor.subTask("Connecting");
RDSPlugin.connectAndReveal(connectionProfile);
monitor.worked(1);
completedSuccessfully = (connectionProfile.getConnectionState() == IConnectionProfile.CONNECTED_STATE);
} catch (Exception e) {
throw new InvocationTargetException(e);
} finally {
monitor.done();
}
}
/**
* Opens a CIDR IP range ingress for the selected DB instance if the user
* has selected to have permissions configured automatically.
*
* @param monitor
* ProgressMonitor for this runnable's progress.
*/
private void configureSecurityGroupPermissions(IProgressMonitor monitor) {
if (wizardDataModel.getConfigurePermissions() == false) {
monitor.worked(5);
return;
}
if (isVpcDbInstance()) {
/*
* DB Instances created with the most recent versions of the RDS
* API will always use VPC security groups, which are owned in
* the user's EC2 account.
*/
openVPCSecurityGroupIngress(new SubProgressMonitor(monitor, 5));
} else {
/*
* For older/legacy DB Instances, we need to modify the RDS
* security groups instead of working with EC2 directly.
*/
openLegacySecurityGroupIngress(new SubProgressMonitor(monitor, 5));
}
}
/**
* Returns true if the selected RDS DB Instance is an RDS VPC DB Instance.
*/
private boolean isVpcDbInstance() {
return wizardDataModel.getDbInstance().getVpcSecurityGroups().isEmpty() == false;
}
public boolean didCompleteSuccessfully() {
return completedSuccessfully;
}
private void openVPCSecurityGroupIngress(IProgressMonitor monitor) {
monitor.beginTask("Configuring database security group", 3);
List<VpcSecurityGroupMembership> vpcSecurityGroups = wizardDataModel.getDbInstance().getVpcSecurityGroups();
if (vpcSecurityGroups == null || vpcSecurityGroups.isEmpty()) {
throw new RuntimeException("Expected a DB instance with VPC security groups!");
}
String vpcSecurityGroupId = vpcSecurityGroups.get(0).getVpcSecurityGroupId();
try {
AmazonEC2 ec2 = AwsToolkitCore.getClientFactory().getEC2Client();
ec2.authorizeSecurityGroupIngress(new AuthorizeSecurityGroupIngressRequest()
.withGroupId(vpcSecurityGroupId)
.withIpPermissions(new IpPermission()
.withFromPort(wizardDataModel.getDbInstance().getEndpoint().getPort())
.withToPort(wizardDataModel.getDbInstance().getEndpoint().getPort())
.withIpProtocol("tcp")
.withIpRanges(wizardDataModel.getCidrIpRange())));
} catch (AmazonServiceException ase) {
// We can safely ignore InvalidPermission.Duplicate errors,
// but will rethrow all other errors.
if (!ase.getErrorCode().equals("InvalidPermission.Duplicate")) {
throw ase;
}
}
}
/**
* This method opens security group permissions for legacy RDS DB Instances.
* Legacy DB Instances do not operate in VPC and connection permissions are
* managed through RDS security groups (not directly through EC2 security
* groups).
*/
private void openLegacySecurityGroupIngress(IProgressMonitor monitor) {
monitor.beginTask("Configuring database security group", 3);
// First make sure our security group exists...
try {
rds.describeDBSecurityGroups(new DescribeDBSecurityGroupsRequest()
.withDBSecurityGroupName(DEFAULT_SECURITY_GROUP_NAME)).getDBSecurityGroups();
} catch (DBSecurityGroupNotFoundException e) {
rds.createDBSecurityGroup(new CreateDBSecurityGroupRequest()
.withDBSecurityGroupName(DEFAULT_SECURITY_GROUP_NAME)
.withDBSecurityGroupDescription(DEFAULT_SECURITY_GROUP_DESCRIPTION));
}
monitor.worked(1);
// Then make sure that it has usable permission...
List<String> existingSecurityGroupNames = new ArrayList<String>();
for (DBSecurityGroupMembership groupMembership : wizardDataModel.getDbInstance().getDBSecurityGroups()) {
existingSecurityGroupNames.add(groupMembership.getDBSecurityGroupName());
}
if (existingSecurityGroupNames.contains(DEFAULT_SECURITY_GROUP_NAME) == false) {
existingSecurityGroupNames.add(DEFAULT_SECURITY_GROUP_NAME);
rds.modifyDBInstance(new ModifyDBInstanceRequest()
.withDBInstanceIdentifier(wizardDataModel.getDbInstance().getDBInstanceIdentifier())
.withDBSecurityGroups(existingSecurityGroupNames));
}
monitor.worked(1);
try {
rds.authorizeDBSecurityGroupIngress(new AuthorizeDBSecurityGroupIngressRequest()
.withCIDRIP(wizardDataModel.getCidrIpRange())
.withDBSecurityGroupName(DEFAULT_SECURITY_GROUP_NAME));
} catch (AuthorizationAlreadyExistsException e) {}
monitor.worked(1);
}
/**
* Returns a driver for connecting to the user's database. If an existing,
* compatible driver is found, it will be used, otherwise a new driver will
* be created and returned.
*
* For more information on creating DriverInstances, see:
* http://stevenmcherry.wordpress.com/2009/04/24/programmatically-creating-dtp-driver-and-profile-definitions/
*
* @return A driver for connecting to the user's database.
*/
private DriverInstance getDriver() {
if (wizardDataModel.isUseExistingDriverDefinition()) {
return wizardDataModel.getDriverDefinition();
}
String targetId = "DriverDefn." + connectionFactory.getDriverTemplate() + "." + connectionFactory.createDriverName();
for (DriverInstance driverInstance : DriverManager.getInstance().getAllDriverInstances()) {
if (driverInstance.getId().equals(targetId)) return driverInstance;
}
Properties driverProperties = new Properties();
if (wizardDataModel.getJdbcDriver() != null) {
// The MySQL driver is currently shipped with the plugins, so in this one case,
// the wizard data model won't have the driver file specified.
driverProperties.setProperty(IDriverMgmtConstants.PROP_DEFN_JARLIST, wizardDataModel.getJdbcDriver().getAbsolutePath());
}
driverProperties.setProperty(IJDBCConnectionProfileConstants.DRIVER_CLASS_PROP_ID, connectionFactory.getDriverClass());
driverProperties.setProperty(IJDBCConnectionProfileConstants.DATABASE_VENDOR_PROP_ID, connectionFactory.getDatabaseVendor());
driverProperties.setProperty(IJDBCConnectionProfileConstants.DATABASE_VERSION_PROP_ID, connectionFactory.getDatabaseVersion());
driverProperties.setProperty(IJDBCConnectionProfileConstants.SAVE_PASSWORD_PROP_ID, String.valueOf(true));
driverProperties.setProperty(IDriverMgmtConstants.PROP_DEFN_TYPE, connectionFactory.getDriverTemplate());
if (connectionFactory.getAdditionalDriverProperties() != null) {
driverProperties.putAll(connectionFactory.getAdditionalDriverProperties());
}
IPropertySet propertySet = new PropertySetImpl(connectionFactory.createDriverName(), targetId);
propertySet.setBaseProperties(driverProperties);
DriverInstance driver = new DriverInstance(propertySet);
DriverManager.getInstance().addDriverInstance(driver);
return driver;
}
/**
* Creates the DTP connection profile by assembling properties from
* IJDBCDriverDefinitionConstants, IJDBCConnectionProfileConstants, and a
* few AWS custom connection profile properties.
*/
private IConnectionProfile createConnectionProfile(DriverInstance driverInstance, IProgressMonitor monitor)
throws ConnectionProfileException {
monitor.beginTask("Creating connection profile", 1);
DBInstance dbInstance = wizardDataModel.getDbInstance();
Properties profileProperties = driverInstance.getPropertySet().getBaseProperties();
profileProperties.setProperty(IJDBCDriverDefinitionConstants.URL_PROP_ID, connectionFactory.createJdbcUrl());
profileProperties.setProperty(IJDBCDriverDefinitionConstants.PASSWORD_PROP_ID, wizardDataModel.getDbPassword());
profileProperties.setProperty(IJDBCDriverDefinitionConstants.USERNAME_PROP_ID, dbInstance.getMasterUsername());
if (dbInstance.getDBName() != null) {
profileProperties.setProperty(IJDBCDriverDefinitionConstants.DATABASE_NAME_PROP_ID, dbInstance.getDBName());
}
profileProperties.setProperty("org.eclipse.datatools.connectivity.driverDefinitionID", driverInstance.getId());
/*
* We add custom connection profile properties to help us easily recognize
* the source RDS instance.
*/
profileProperties.setProperty(RDSDriverDefinitionConstants.DB_INSTANCE_ID, dbInstance.getDBInstanceIdentifier());
profileProperties.setProperty(RDSDriverDefinitionConstants.DB_REGION_ID, RegionUtils.getCurrentRegion().getId());
profileProperties.setProperty(RDSDriverDefinitionConstants.DB_ACCCOUNT_ID, AwsToolkitCore.getDefault().getCurrentAccountId());
String profileName = "Amazon RDS DB: " + dbInstance.getDBInstanceIdentifier() + " - " + RegionUtils.getCurrentRegion().getName();
/*
* if the connection profile already exists... just modify it
*/
IConnectionProfile existingProfile = ProfileManager.getInstance().getProfileByName(profileName);
if (existingProfile != null) {
existingProfile.setBaseProperties(profileProperties);
ProfileManager.getInstance().modifyProfile(existingProfile);
monitor.worked(1);
return existingProfile;
} else {
IConnectionProfile profile = ProfileManager.getInstance().createProfile(
profileName, profileName,
connectionFactory.getConnectionProfileProviderId(),
profileProperties);
monitor.worked(1);
return profile;
}
}
}