/************************************************************************* * Copyright 2009-2014 Eucalyptus Systems, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. * * Please contact Eucalyptus Systems, Inc., 6755 Hollister Ave., Goleta * CA 93117, USA or visit http://www.eucalyptus.com/licenses/ if you need * additional information or have any questions. ************************************************************************/ package com.eucalyptus.database.activities; import java.io.IOException; import java.security.KeyPair; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.List; import javax.crypto.Cipher; import org.apache.log4j.Logger; import org.bouncycastle.util.encoders.Base64; import com.eucalyptus.auth.Accounts; import com.eucalyptus.auth.AuthException; import com.eucalyptus.auth.euare.ServerCertificateMetadataType; import com.eucalyptus.auth.euare.ServerCertificateType; import com.eucalyptus.auth.util.X509CertHelper; import com.eucalyptus.bootstrap.DatabaseInfo; import com.eucalyptus.cloudformation.CloudFormation; import com.eucalyptus.cloudformation.Parameter; import com.eucalyptus.cloudformation.Stack; import com.eucalyptus.component.Topology; import com.eucalyptus.compute.common.ClusterInfoType; import com.eucalyptus.compute.common.DescribeKeyPairsResponseItemType; import com.eucalyptus.compute.common.ImageDetails; import com.eucalyptus.compute.common.Volume; import com.eucalyptus.configurable.StaticDatabasePropertyEntry; import com.eucalyptus.crypto.Certs; import com.eucalyptus.crypto.Ciphers; import com.eucalyptus.crypto.Crypto; import com.eucalyptus.crypto.util.PEMFiles; import com.eucalyptus.resources.AbstractEventHandler; import com.eucalyptus.resources.EventHandlerChain; import com.eucalyptus.resources.EventHandlerException; import com.eucalyptus.resources.client.CloudFormationClient; import com.eucalyptus.resources.client.Ec2Client; import com.eucalyptus.resources.client.EuareClient; import com.eucalyptus.util.DNSProperties; import com.eucalyptus.util.EucalyptusCloudException; import com.eucalyptus.util.Exceptions; import com.google.common.base.Charsets; import com.google.common.base.Joiner; import com.google.common.collect.Lists; import com.google.common.io.Resources; public class EventHandlerChainCreateDbInstance extends EventHandlerChain<NewDBInstanceEvent> { private static Logger LOG = Logger.getLogger( EventHandlerChainCreateDbInstance.class ); private static final String DATABASE_VM_STACK_NAME = "euca-internal-db-service"; private static final String SERVER_CERTIFICATE_NAME = "euca-internal-db-service"; private static final String DEFAULT_SERVER_CERT_PATH = "/euca-internal"; @Override public EventHandlerChain<NewDBInstanceEvent> build() { this.append(new AdmissionControl(this)); this.append(new CreateStack(this)); return this; } public static String getStackName(String accountId) { return String.format("%s-%s", DATABASE_VM_STACK_NAME , accountId); } public static String getCertificateName(String accountId) { return String.format("%s-%s", SERVER_CERTIFICATE_NAME, accountId); } public static class CreateStack extends AbstractEventHandler<NewDBInstanceEvent> { private String dbName; public CreateStack(EventHandlerChain<NewDBInstanceEvent> chain) { super(chain); } @Override public void apply(NewDBInstanceEvent evt) throws EventHandlerException { try { if (!Topology.isEnabled(CloudFormation.class)) throw new EventHandlerException("CloudFormation is not enabled"); this.dbName = evt.getDbName(); final String accountId = getAccountByUser(evt.getUserId()); final String certificateName = getCertificateName(accountId); // generate certificate X509Certificate kpCert = null; ServerCertificateMetadataType certMetadata = findCertificate(evt.getUserId()); if (certMetadata != null) { LOG.debug("Certificate already exists, there is no reason to create new one"); ServerCertificateType sType = EuareClient.getInstance().getServerCertificate(evt.getUserId(), certificateName); kpCert = X509CertHelper.pemToCertificate(sType.getCertificateBody()); } else { String certPem = null; String pkPem = null; try { final KeyPair kp = Certs.generateKeyPair(); kpCert = Certs.generateCertificate(kp, String .format("Certificate for (%s)", certificateName)); certPem = new String(PEMFiles.getBytes(kpCert)); pkPem = new String(PEMFiles.getBytes(kp)); } catch (final Exception ex) { throw new EventHandlerException("failed generating server cert", ex); } try { certMetadata = EuareClient.getInstance().uploadServerCertificate(evt.getUserId(), certificateName, DEFAULT_SERVER_CERT_PATH, certPem, pkPem, null); } catch (final Exception ex) { throw new EventHandlerException("failed to upload server cert", ex); } LOG.debug("Created new certificate " + certMetadata.getServerCertificateName()); } String encryptedPassword = null; try{ final Cipher cipher = Ciphers.RSA_PKCS1.get(); cipher.init(Cipher.ENCRYPT_MODE, kpCert.getPublicKey(), Crypto.getSecureRandomSupplier( ).get( )); byte[] bencPassword = cipher.doFinal(evt.getMasterUserPassword().getBytes()); encryptedPassword = new String(Base64.encode(bencPassword)); }catch(final Exception ex) { LOG.error("Failed to encrypt DB password"); throw new EventHandlerException("Failed to encrypt the password"); } // use CF for stack creation String template = loadTemplate("database-cf-template.json"); ArrayList<Parameter> params = new ArrayList<Parameter>(); params.add(new Parameter("KeyName", DatabaseServerProperties.KEYNAME)); params.add(new Parameter("CERTARN", certMetadata.getArn())); params.add(new Parameter("ImageId", DatabaseServerProperties.IMAGE)); params.add(new Parameter("VmExpirationDays", DatabaseServerProperties.EXPIRATION_DAYS)); params.add(new Parameter("InstanceType", DatabaseServerProperties.INSTANCE_TYPE)); params.add(new Parameter("NtpServer", DatabaseServerProperties.NTP_SERVER)); params.add(new Parameter("PasswordEncrypted", encryptedPassword)); params.add(new Parameter("VolumeId", DatabaseServerProperties.VOLUME)); if (DatabaseInfo.getDatabaseInfo().getAppendOnlyPort() != null && !DatabaseInfo.getDatabaseInfo().getAppendOnlyPort().isEmpty()) params.add(new Parameter("DBPort", Integer.toString(evt.getPort()))); if (DatabaseServerProperties.INIT_SCRIPT != null) { params.add(new Parameter("InitScript", DatabaseServerProperties.INIT_SCRIPT)); } List<String> zones = DatabaseServerProperties.listConfiguredZones(); params.add(new Parameter("AvailabilityZones", Joiner.on(",").join( zones ))); params.add(new Parameter("EuareServiceUrl", String.format("euare.%s", DNSProperties.getDomain()))); params.add(new Parameter("ComputeServiceUrl", String.format("compute.%s", DNSProperties.getDomain()))); LOG.debug("Creating CF stack for the database worker acct: " + accountId); CloudFormationClient.getInstance().createStack(evt.getUserId(), getStackName(accountId), template, params); LOG.debug("Done creating CF stack for the database worker acct: " + accountId); } catch (EventHandlerException ex) { throw ex; } catch (final Exception ex) { throw new EventHandlerException(ex.getMessage()); } } @Override public void rollback() throws EventHandlerException { // set configured back to false if create failed for system user if (DatabaseServerProperties.REPORTING_DB_NAME.equals(this.dbName)) { try { StaticDatabasePropertyEntry.update( "com.eucalyptus.database.activities.DatabaseServerProperties.configured", "services.database.worker.configured", "false" ); } catch (Exception e) { LOG.warn("Can't set services.database.worker.configured to false dues to: " + e.getMessage()); } } } private ServerCertificateMetadataType findCertificate(String userId) throws EucalyptusCloudException { try { return EuareClient.getInstance().describeServerCertificate(userId, getCertificateName(getAccountByUser(userId)), DEFAULT_SERVER_CERT_PATH); } catch (Exception ex) { throw new EucalyptusCloudException("failed to describe server cert", ex); } } private String loadTemplate(final String resourceName) { try { return Resources.toString( Resources.getResource(getClass(), resourceName), Charsets.UTF_8); } catch (final IOException e) { throw Exceptions.toUndeclared(e); } } } public static String getAccountByUser(String userId) throws EventHandlerException { try{ return Accounts.lookupPrincipalByUserId(userId).getAccountNumber(); }catch(final AuthException ex){ throw new EventHandlerException("Failed to lookup user's account", ex); } } // make sure the cloud is ready to launch database server instances // e.g., lack of resources will keep the launcher from creating resources public static class AdmissionControl extends AbstractEventHandler<NewDBInstanceEvent> { private String dbName; public AdmissionControl(EventHandlerChain<NewDBInstanceEvent> chain) { super(chain); } @Override public void apply(NewDBInstanceEvent evt) throws EventHandlerException { boolean stackFound = false; final String userId = evt.getUserId(); final String accountId = getAccountByUser(userId); String stackName = getStackName(accountId); this.dbName = evt.getDbName(); try{ Stack stack = CloudFormationClient.getInstance().describeStack(userId, stackName); if (stack != null) { stackFound = true; } }catch(final Exception ex){ stackFound = false; } if(stackFound) throw new EventHandlerException("Existing stack ("+ stackName +") found"); // this will stop the whole instance launch chain final String emi = DatabaseServerProperties.IMAGE; List<ImageDetails> images = null; try{ images = Ec2Client.getInstance().describeImages(null, Lists.newArrayList(emi)); if(images==null || images.size()<=0 ||! images.get(0).getImageId().toLowerCase().equals(emi.toLowerCase())) throw new EventHandlerException("No such EMI is found: "+emi); }catch(final EventHandlerException ex){ throw ex; }catch(final Exception ex){ throw new EventHandlerException("failed to validate the db server EMI", ex); } final String volumeId = DatabaseServerProperties.VOLUME; Volume volumeToUse = null; try{ List<Volume> volumes = Ec2Client.getInstance().describeVolumes(null, Lists.newArrayList(volumeId)); if(volumes==null || volumes.size()<=0 || ! volumes.get(0).getVolumeId().toLowerCase().equals(volumeId.toLowerCase())) throw new EventHandlerException("No such volume id is found: " + volumeId); if(! "available".equals(volumes.get(0).getStatus())) throw new EventHandlerException("Volume is not available"); else volumeToUse = volumes.get(0); }catch(final EventHandlerException ex) { throw ex; }catch(final Exception ex) { throw new EventHandlerException("failed to validate the db server volume ID", ex); } // check if volume is in a configured zone List<String> configuredZones = null; try { configuredZones = DatabaseServerProperties.listConfiguredZones(); } catch (Exception e) { LOG.error("Can't validate AZ for volume due to: " + e.getMessage()); } if (configuredZones != null && !configuredZones.contains(volumeToUse.getAvailabilityZone())) throw new EventHandlerException("Volume is in an AZ that is not configured for database service"); List<ClusterInfoType> clusters = null; try{ clusters = Ec2Client.getInstance().describeAvailabilityZones(null, true); }catch(final Exception ex){ throw new EventHandlerException("failed to validate the zones", ex); } // are there enough resources in the zone? final String instanceType = DatabaseServerProperties.INSTANCE_TYPE; int numVm = 1; final int capacity = findAvailableResources(clusters, volumeToUse.getAvailabilityZone(), instanceType); if(capacity<numVm) throw new EventHandlerException("not enough resource in the zone " + volumeToUse.getAvailabilityZone()); // check if the keyname is configured and exists final String keyName = DatabaseServerProperties.KEYNAME; if(keyName!=null && keyName.length()>0){ try{ final List<DescribeKeyPairsResponseItemType> keypairs = Ec2Client.getInstance().describeKeyPairs(null, Lists.newArrayList(keyName)); if(keypairs==null || keypairs.size()<=0 || !keypairs.get(0).getKeyName().equals(keyName)) throw new Exception(); }catch(Exception ex){ throw new EventHandlerException(String.format("The configured keyname %s is not found", DatabaseServerProperties.KEYNAME)); } } } private int findAvailableResources(final List<ClusterInfoType> clusters, final String zoneName, final String instanceType){ // parse euca-describe-availability-zones verbose response // WARNING: this is not a standard API! for(int i =0; i<clusters.size(); i++){ final ClusterInfoType cc = clusters.get(i); if(zoneName.equals(cc.getZoneName())){ for(int j=i+1; j< clusters.size(); j++){ final ClusterInfoType candidate = clusters.get(j); if(candidate.getZoneName()!=null && candidate.getZoneName().toLowerCase().contains(instanceType.toLowerCase())){ //<zoneState>0002 / 0002 2 512 10</zoneState> final String state = candidate.getZoneState(); final String[] tokens = state.split("/"); if(tokens!=null && tokens.length>0){ try{ String strNum = tokens[0].trim().replaceFirst("0+", ""); if(strNum.length()<=0) strNum="0"; return Integer.parseInt(strNum); }catch(final NumberFormatException ex){ break; }catch(final Exception ex){ break; } } } } break; } } return Integer.MAX_VALUE; // when check fails, let's assume its abundant } @Override public void rollback() { // set configured back to false if create failed for system user if (DatabaseServerProperties.REPORTING_DB_NAME.equals(this.dbName)) { try { StaticDatabasePropertyEntry.update( "com.eucalyptus.database.activities.DatabaseServerProperties.configured", "services.database.worker.configured", "false" ); } catch (Exception e) { LOG.warn("Can't set services.database.worker.configured to false dues to: " + e.getMessage()); } } } } }