/*******************************************************************************
* Copyright (c) 2012 GigaSpaces Technologies Ltd. All rights reserved
*
* 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://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License 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 org.cloudifysource.esc.driver.provisioning;
import java.io.File;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.cloudifysource.domain.cloud.Cloud;
import org.cloudifysource.domain.cloud.compute.ComputeTemplate;
import org.cloudifysource.dsl.internal.CloudifyConstants;
import org.openspaces.admin.Admin;
/**
* @author noak
* @since 2.0.1
*/
public abstract class BaseProvisioningDriver extends BaseComputeDriver {
private static final String PRIVATE_KEY_PREFIX = "-----BEGIN RSA PRIVATE KEY-----";
protected static final int MULTIPLE_SHUTDOWN_REQUEST_IGNORE_TIMEOUT = 120000;
protected static final int WAIT_THREAD_SLEEP_MILLIS = 10000;
protected static final int WAIT_TIMEOUT_MILLIS = 360000;
// TODO - make this a configuration option
protected static final int MAX_SERVERS_LIMIT = 200;
protected static final String EVENT_WAITING_FOR_NODE_TO_BE_AVAILABLE = "waiting_for_node_to_be_available";
protected static final String EVENT_STARTING_MACHINE_WITH_NAME = "starting_machine_with_name";
protected static final String EVENT_MACHINE_STARTED = "machine_started";
protected static final String EVENT_ATTEMPT_CONNECTION_TO_CLOUD_API = "try_to_connect_to_cloud_api";
protected static final String EVENT_ACCOMPLISHED_CONNECTION_TO_CLOUD_API = "connection_to_cloud_api_succeeded";
protected static final String EVENT_ATTEMPT_START_MGMT_VMS = "attempting_to_create_management_vms";
protected static final String EVENT_RETRIEVE_EXISTING_MANAGEMENT_MACHINES = "retrieving_existing_management";
protected static final String EVENT_DESTROYING_MACHINES = "destroying_machines_with_private_ips";
protected static final String EVENT_MACHINES_DESTROYED_SUCCESSFULLY = "machines_destroyed_succesfully";
protected static final String EVENT_MGMT_VMS_STARTED = "management_started_successfully";
protected static final String AGENT_MACHINE_PREFIX = "cloudify-agent-";
protected static final String MANAGMENT_MACHINE_PREFIX = "cloudify-managememnt-";
protected boolean management;
protected static AtomicInteger counter = new AtomicInteger();
protected String serverNamePrefix;
protected String cloudName;
protected String cloudTemplateName;
protected Admin admin;
protected Cloud cloud;
protected final java.util.logging.Logger logger = java.util.logging.Logger.getLogger(this.getClass().getName());
protected Boolean cleanRemoteDirectoryOnStart = false;
protected boolean isVerboseValidation = true;
private AtomicInteger zonesIndex = new AtomicInteger(0);
/**
* Initializing the cloud deployer according to the given cloud configuration.
*
* @param cloud
* Cloud object to use
*/
protected abstract void initDeployer(final Cloud cloud);
public String getCloudTemplateName() {
return cloudTemplateName;
}
public void setCloudTemplateName(String cloudTemplateName) {
this.cloudTemplateName = cloudTemplateName;
}
@Override
public String getCloudName() {
return this.cloudName;
}
@Override
public void setConfig(final ComputeDriverConfiguration configuration) throws CloudProvisioningException {
super.setConfig(configuration);
this.cloud = configuration.getCloud();
this.cloudTemplateName = configuration.getCloudTemplate();
this.management = configuration.isManagement();
this.cloudName = cloud.getName();
this.admin = configuration.getAdmin();
Object bol = cloud.getCustom().get(CloudifyConstants.CUSTOM_PROPERTY_VERBOSE_VALIDATION);
if (bol == null) {
this.isVerboseValidation = true;
} else if (bol instanceof String) {
this.isVerboseValidation = Boolean.parseBoolean((String) bol);
} else if (bol instanceof Boolean) {
this.isVerboseValidation =
(Boolean) cloud.getCustom().get(CloudifyConstants.CUSTOM_PROPERTY_VERBOSE_VALIDATION);
}
publishEvent(EVENT_ATTEMPT_CONNECTION_TO_CLOUD_API, cloud.getProvider().getProvider());
initDeployer(cloud);
publishEvent(EVENT_ACCOMPLISHED_CONNECTION_TO_CLOUD_API, cloud.getProvider().getProvider());
logger.fine("Initializing Cloud Provisioning - management mode: " + management + ". Using template: "
+ cloudTemplateName + " with cloud: " + cloudName);
String prefix =
management ? cloud.getProvider().getManagementGroup() : cloud.getProvider().getMachineNamePrefix();
if (StringUtils.isBlank(prefix)) {
if (management) {
prefix = MANAGMENT_MACHINE_PREFIX;
} else {
prefix = AGENT_MACHINE_PREFIX;
}
logger.warning("Prefix for machine name was not set. Using: " + prefix);
}
this.serverNamePrefix = prefix;
initCleanRemoteOnStart(cloud);
}
@Override
public void onServiceUninstalled(final long duration, final TimeUnit unit)
throws InterruptedException, TimeoutException, CloudProvisioningException {
}
/**
*
* @param serverName .
* @param endTime .
* @param template .
* @return
* @throws CloudProvisioningException .
* @throws TimeoutException .
*/
protected abstract MachineDetails createServer(
final String serverName,
final long endTime,
final ComputeTemplate template) throws CloudProvisioningException, TimeoutException;
/**
*
* @param numberOfManagementMachines .
* @param numberOfErrors .
* @param firstCreationException .
* @param createdManagementMachines .
* @throws CloudProvisioningException .
*/
protected abstract void handleProvisioningFailure(
final int numberOfManagementMachines,
final int numberOfErrors,
final Exception firstCreationException,
final MachineDetails[] createdManagementMachines) throws CloudProvisioningException;
/**
* Handles credentials for accessing the server - in this order: 1. pem file (set as a key file on the user block in
* the groovy file) 2. machine's remote password (set previously by the cloud driver)
*
* @param machineDetails
* The MachineDetails object that represents this server
* @param template
* the cloud template.
* @throws CloudProvisioningException
* Indicates missing credentials or IOException (when a key file is used)
*/
protected void handleServerCredentials(final MachineDetails machineDetails, final ComputeTemplate template)
throws CloudProvisioningException {
File keyFile = null;
// using a key (pem) file
if (machineDetails.getKeyFile() != null) {
keyFile = machineDetails.getKeyFile();
if (!keyFile.isFile()) {
throw new CloudProvisioningException("The specified key file could not be found: "
+ keyFile.getAbsolutePath());
}
} else if (StringUtils.isNotBlank(template.getKeyFile())) {
final String keyFileStr = template.getKeyFile();
// fixConfigRelativePaths(cloud, template);
keyFile = new File(keyFileStr);
if (!keyFile.isAbsolute()) {
keyFile = new File(template.getAbsoluteUploadDir(), keyFileStr);
}
if (!keyFile.exists()) {
throw new CloudProvisioningException("The specified key file could not be found: "
+ keyFile.getAbsolutePath());
}
} else {
// using a password
final String remotePassword = machineDetails.getRemotePassword();
if (StringUtils.isNotBlank(remotePassword)) {
// is this actually a private key file?
if (remotePassword.startsWith(PRIVATE_KEY_PREFIX)) {
logger.fine("Cloud has provided a key file for connections to new machines");
try {
keyFile = File.createTempFile("gs-esm-key", ".pem");
keyFile.deleteOnExit();
FileUtils.write(keyFile, remotePassword);
// template.setKeyFile(keyFile.getAbsolutePath());
machineDetails.setKeyFile(keyFile);
} catch (final IOException e) {
throw new CloudProvisioningException("Failed to create a temporary "
+ "file for cloud server's key file", e);
}
} else {
// this is a password
logger.fine("Cloud has provided a password for remote connections to new machines");
}
} else {
// if we got here - there is no key file or password on the
// cloud or node.
logger.severe("No Password or key file specified in the cloud configuration file - connection to"
+ " the new machine is not possible.");
throw new CloudProvisioningException(
"No credentials (password or key file) supplied with the cloud configuration file");
}
}
logServerDetails(machineDetails, keyFile);
}
/**
* Publish a provisioning event occurred for the listeners registered on this class.
*
* @param eventName
* The name of the event (must be in the message bundle)
* @param args
* Arguments that complement the event message
*/
protected void publishEvent(final String eventName, final Object... args) {
for (final ProvisioningDriverListener listener : this.eventsListenersList) {
listener.onProvisioningEvent(eventName, args);
}
}
/*********
* Created a machine details with basic settings from the given cloud template.
*
* @param template
* the cloud template.
* @return the newly created machine details.
*/
protected MachineDetails createMachineDetailsForTemplate(final ComputeTemplate template) {
final MachineDetails md = new MachineDetails();
md.setAgentRunning(false);
md.setCloudifyInstalled(false);
md.setInstallationDirectory(null);
md.setRemoteUsername(template.getUsername());
md.setRemotePassword(template.getPassword());
md.setRemoteExecutionMode(template.getRemoteExecution());
md.setFileTransferMode(template.getFileTransfer());
md.setScriptLangeuage(template.getScriptLanguage());
md.setCleanRemoteDirectoryOnStart(this.cleanRemoteDirectoryOnStart);
return md;
}
/*********
* .
*
* @param endTime
* .
* @param numberOfManagementMachines
* .
* @return .
* @throws TimeoutException .
* @throws CloudProvisioningException .
*/
protected MachineDetails[] doStartManagementMachines(final long endTime, final int numberOfManagementMachines)
throws TimeoutException, CloudProvisioningException {
final ExecutorService executors = Executors.newFixedThreadPool(numberOfManagementMachines);
@SuppressWarnings("unchecked")
final Future<MachineDetails>[] futures = (Future<MachineDetails>[]) new Future<?>[numberOfManagementMachines];
final ComputeTemplate managementTemplate =
this.cloud.getCloudCompute().getTemplates().get(
this.cloud.getConfiguration().getManagementMachineTemplate());
try {
// Call startMachine asynchronously once for each management machine
for (int i = 0; i < numberOfManagementMachines; i++) {
final int index = i + 1;
futures[i] = executors.submit(new Callable<MachineDetails>() {
@Override
public MachineDetails call()
throws Exception {
return createServer(serverNamePrefix + index, endTime, managementTemplate);
}
});
}
// Wait for each of the async calls to terminate.
int numberOfErrors = 0;
Exception firstCreationException = null;
final MachineDetails[] createdManagementMachines = new MachineDetails[numberOfManagementMachines];
for (int i = 0; i < createdManagementMachines.length; i++) {
try {
createdManagementMachines[i] = futures[i].get(endTime - System.currentTimeMillis(),
TimeUnit.MILLISECONDS);
} catch (final InterruptedException e) {
++numberOfErrors;
publishEvent("failed_to_create_management_vm", e.getMessage());
logger.log(Level.SEVERE, "Failed to start a management machine", e);
if (firstCreationException == null) {
firstCreationException = e;
}
} catch (final ExecutionException e) {
++numberOfErrors;
publishEvent("failed_to_create_management_vm", e.getMessage());
logger.log(Level.SEVERE, "Failed to start a management machine", e);
if (firstCreationException == null) {
firstCreationException = e;
}
}
}
// In case of a partial error, shutdown all servers that did start up
if (numberOfErrors > 0) {
handleProvisioningFailure(numberOfManagementMachines, numberOfErrors, firstCreationException,
createdManagementMachines);
}
return createdManagementMachines;
} finally {
if (executors != null) {
executors.shutdownNow();
}
}
}
/**
* Gets the next availability zone if a list of zones is set in the template (round robin).
* @param template The compute template to get the list of availability zones from
* @return Zone name or an empty string if non is specified
*/
protected String getAvailabilityZone(final ComputeTemplate template) {
List<String> zones = template.getAvailabilityZones();
String zone = "";
if (zones != null && !zones.isEmpty()) {
zone = zones.get(zonesIndex.getAndIncrement() % zones.size());
}
return zone;
}
/**
* returns the message as it appears in the DefaultProvisioningDriver message bundle.
*
* @param messageBundle
* The message bundle containing the specified message
* @param msgName
* the message key as it is defined in the message bundle.
* @param arguments
* the message arguments
* @return the formatted message according to the message key.
*/
protected String getFormattedMessage(final ResourceBundle messageBundle, final String msgName,
final Object... arguments) {
final String message = messageBundle.getString(msgName);
if (message == null) {
logger.warning("Missing resource in messages resource bundle: " + msgName);
return msgName;
}
try {
return MessageFormat.format(message, arguments);
} catch (final IllegalArgumentException e) {
logger.fine("Failed to format message: " + msgName + " with format: "
+ message + " and arguments: " + Arrays.toString(arguments));
return msgName;
}
}
private void initCleanRemoteOnStart(final Cloud cloud) {
// set custom settings
final Map<String, Object> customSettings = cloud.getCustom();
if (customSettings != null) {
// clean GS files on shutdown
if (customSettings.containsKey(CloudifyConstants.CUSTOM_PROPERTY_CLEAN_REMOTE_DIR_ON_START)) {
final Object cleanRemoteDirValue =
customSettings.get(CloudifyConstants.CUSTOM_PROPERTY_CLEAN_REMOTE_DIR_ON_START);
if (cleanRemoteDirValue instanceof Boolean) {
this.cleanRemoteDirectoryOnStart = (Boolean) cleanRemoteDirValue;
} else if (cleanRemoteDirValue instanceof String) {
this.cleanRemoteDirectoryOnStart = Boolean.parseBoolean((String) cleanRemoteDirValue);
} else {
throw new IllegalArgumentException("Unexpected value for BYON property: "
+ CloudifyConstants.CUSTOM_PROPERTY_CLEAN_REMOTE_DIR_ON_START
+ ". Was expecting a boolean or String, got: "
+ cleanRemoteDirValue.getClass().getName());
}
}
}
}
private void logServerDetails(final MachineDetails machineDetails, final File tempFile) {
if (logger.isLoggable(Level.FINE)) {
final String nodePrefix = "[" + machineDetails.getMachineId() + "] ";
logger.fine(nodePrefix + "Cloud Server is allocated.");
if (tempFile == null) {
logger.fine(nodePrefix + "Password: ***");
} else {
logger.fine(nodePrefix + "Key File: " + tempFile.getAbsolutePath());
}
if (logger.isLoggable(Level.FINE)) {
logger.fine("Private IP: " + machineDetails.getPrivateAddress());
logger.fine("Public IP: " + machineDetails.getPublicAddress());
}
}
}
}