package com.sixsq.slipstream.connector;
/*
* +=================================================================+
* SlipStream Server (WAR)
* =====
* Copyright (C) 2014 SixSq Sarl (sixsq.com)
* =====
* 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.
* -=================================================================-
*/
import com.sixsq.slipstream.configuration.Configuration;
import com.sixsq.slipstream.credentials.Credentials;
import com.sixsq.slipstream.exceptions.ConfigurationException;
import com.sixsq.slipstream.exceptions.ProcessException;
import com.sixsq.slipstream.exceptions.SlipStreamClientException;
import com.sixsq.slipstream.exceptions.SlipStreamException;
import com.sixsq.slipstream.exceptions.SlipStreamInternalException;
import com.sixsq.slipstream.exceptions.SlipStreamRuntimeException;
import com.sixsq.slipstream.exceptions.ValidationException;
import com.sixsq.slipstream.factory.ModuleParametersFactoryBase;
import com.sixsq.slipstream.persistence.ImageModule;
import com.sixsq.slipstream.persistence.Run;
import com.sixsq.slipstream.persistence.User;
import com.sixsq.slipstream.util.ProcessUtils;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Logger;
public abstract class CliConnectorBase extends ConnectorBase {
public static final String CLI_LOCATION = "/usr/bin";
protected Logger log;
@Override
abstract public Credentials getCredentials(User user);
@Override
abstract public String getCloudServiceName();
abstract protected String getCloudConnectorPythonModule();
abstract protected Map<String, String> getConnectorSpecificUserParams(User user)
throws ConfigurationException, ValidationException;
/**
* The items in the CLI parameters/options Map are interpreted and processed as follows.
* - item <String, String> indicates a parameter. E.g., --endpoint <URL>.
* When provided to the CLI, the values of the items will be wrapped into
* single quotes. E.g., --endpoint 'https://exmpale.com'. This allows empty
* strings for values as well.
* - item <String, null> indicates an option. E.g, --public-ip.
*/
abstract protected Map<String, String> getConnectorSpecificLaunchParams(Run run, User user)
throws ConfigurationException, ValidationException;
protected Map<String, String> getConnectorSpecificEnvironment(Run run, User user)
throws ConfigurationException, ValidationException {
return new HashMap<String, String>();
}
protected void validateLaunch(Run run, User user) throws ValidationException {
validateCredentials(user);
}
protected void validateDescribe(User user) throws ValidationException {
validateCredentials(user);
}
protected void validateTerminate(Run run, User user) throws ValidationException {
validateCredentials(user);
}
protected String getCloudConnectorBundleUrl(User user) throws ValidationException {
return getCloudParameterValue(user, UserParametersFactoryBase.UPDATE_CLIENTURL_PARAMETER_NAME);
}
public CliConnectorBase(String instanceName) {
super(instanceName);
this.log = Logger.getLogger(this.getClass().getName());
}
@Override
public Run launch(Run run, User user) throws SlipStreamException {
validateLaunch(run, user);
String command = getCommandRunInstances() + createCliParameters(getLaunchParams(run, user));
String result;
String[] commands = { "sh", "-c", command };
try {
result = ProcessUtils.execGetOutput(commands, false, getCommandEnvironment(run, user));
} catch (IOException e) {
e.printStackTrace();
throw (new SlipStreamInternalException(e));
} catch (ProcessException e) {
try {
String[] instanceData = parseRunInstanceResult(e.getStdOut(), this.log);
updateInstanceIdAndIpOnRun(run, instanceData[0], instanceData[1]);
} catch (Exception ex) { }
throw e;
} finally {
cleanupAfterLaunch();
}
String[] instanceData = parseRunInstanceResult(result, this.log);
String instanceId = instanceData[0];
String ipAddress = instanceData[1];
updateInstanceIdAndIpOnRun(run, instanceId, ipAddress);
return run;
}
@Override
public Map<String, Properties> describeInstances(User user, int timeout) throws SlipStreamException {
validateCredentials(user);
String command = getCommandDescribeInstances() + createTimeoutParameter(timeout)
+ createCliParameters(getUserParams(user));
String result;
String[] commands = { "sh", "-c", command };
try {
result = ProcessUtils.execGetOutput(commands, getCommonEnvironment(user), false);
} catch (IOException e) {
e.printStackTrace();
throw (new SlipStreamInternalException(e));
} finally {
cleanupAfterDescribeInstances();
}
return parseDescribeInstanceResult(result);
}
private String createTimeoutParameter(int timeout) {
return " -t " + timeout + " ";
}
@Override
public void terminate(Run run, User user) throws SlipStreamException {
validateCredentials(user);
List<String> instanceIds = getCloudNodeInstanceIds(run);
if(instanceIds.isEmpty()){
throw new SlipStreamClientException("There is no instances to terminate");
}
StringBuilder instances = new StringBuilder();
for (String id : instanceIds) {
instances.append(id.trim()).append("\n");
}
log.info(getConnectorInstanceName() + ". Terminating all instances on run " + run.getUuid());
File tempFile;
try {
tempFile = File.createTempFile("instance-ids", ".tmp");
BufferedWriter bw = new BufferedWriter(new FileWriter(tempFile));
bw.write(instances.toString());
bw.close();
} catch (IOException ex) {
throw new SlipStreamRuntimeException(ex.getMessage(), ex);
}
String command = getCommandTerminateInstances() +
createCliParameters(getUserParams(user)) +
" --instance-ids-file " + tempFile.getPath();
String[] commands = { "sh", "-c", command};
try {
ProcessUtils.execGetOutput(commands, getCommonEnvironment(user));
} catch (SlipStreamClientException e) {
log.info(getConnectorInstanceName() + ". Failed to terminate instances on run " + run.getUuid());
} catch (IOException e) {
log.info(getConnectorInstanceName() + ". IO error while terminating instances on run " + run.getUuid());
} finally {
if (!tempFile.delete()) {
getLog().warning("Cannot delete temporary file: " + tempFile.getPath());
}
cleanupAfterTerminate();
}
}
private void cleanupAfterLaunch() {
deleteTempSshKeyFile();
connectorCleanupAfterCliCall();
}
private void cleanupAfterDescribeInstances() {
connectorCleanupAfterCliCall();
}
private void cleanupAfterTerminate() {
connectorCleanupAfterCliCall();
}
protected void connectorCleanupAfterCliCall() {
//
}
private String createCliParameters(Map<String, String> params) {
String cliParams = "";
for (Map.Entry<String, String> param: params.entrySet()) {
String value = param.getValue();
cliParams += "--" + param.getKey() + " ";
if (value != null) {
cliParams += wrapInSingleQuotes(value) + " ";
}
}
return cliParams;
}
private Map<String, String> getLaunchParams(Run run, User user)
throws ConfigurationException, SlipStreamClientException {
Map<String, String> launchParams = new HashMap<String, String>();
launchParams.putAll(getUserParams(user));
launchParams.putAll(getGenericLaunchParams(run, user));
launchParams.putAll(getConnectorSpecificLaunchParams(run, user));
return launchParams;
}
protected Map<String, String> getGenericLaunchParams(Run run, User user)
throws ConfigurationException, SlipStreamClientException {
Map<String, String> launchParams = new HashMap<String, String>();
launchParams.put("image-id", getImageId(run, user));
launchParams.put("network-type", getNetwork(run));
putLaunchParamPlatform(launchParams, run);
putLaunchParamLoginUserAndPassword(launchParams, run);
putLaunchParamExtraDiskVolatile(launchParams, run);
putLaunchParamRootDiskSize(launchParams, run);
putLaunchParamNativeContextualization(launchParams, run);
return launchParams;
}
private void putLaunchParamPlatform(Map<String, String> launchParams, Run run) throws ValidationException {
if (!isInOrchestrationContext(run)) {
launchParams.put("platform", ((ImageModule) run.getModule()).getPlatform());
}
}
private void putLaunchParamLoginUserAndPassword(Map<String, String> launchParams, Run run) throws ValidationException {
try {
launchParams.put("login-username", getLoginUsername(run));
} catch (ConfigurationException e) {
}
String loginPassword = null;
try {
loginPassword = getLoginPassword(run);
} catch (ConfigurationException e) {
} catch (ValidationException e) {
}
if (loginPassword != null && !loginPassword.isEmpty()) {
launchParams.put("login-password", loginPassword);
}
}
private void putLaunchParamRootDiskSize(Map<String, String> launchParams, Run run) throws ValidationException {
if (!isInOrchestrationContext(run)) {
String rootDiskGb = getRootDisk((ImageModule) run.getModule());
if (rootDiskGb != null && !rootDiskGb.isEmpty()) {
launchParams.put("disk", rootDiskGb);
}
}
}
private void putLaunchParamExtraDiskVolatile(Map<String, String> launchParams, Run run) throws ValidationException {
if (!isInOrchestrationContext(run)) {
String extraDiskGb = getExtraDiskVolatile((ImageModule) run.getModule());
if (extraDiskGb != null && !extraDiskGb.isEmpty()) {
launchParams.put("extra-disk-volatile", extraDiskGb);
}
}
}
protected void putLaunchParamNativeContextualization(Map<String, String> launchParams, Run run)
throws ValidationException {
String key = SystemConfigurationParametersFactoryBase.NATIVE_CONTEXTUALIZATION_KEY;
String nativeContextualization = Configuration.getInstance().getProperty(constructKey(key));
if (nativeContextualization != null) {
launchParams.put(key, nativeContextualization);
}
}
protected void putLaunchParamSecurityGroups(Map<String, String> launchParams, Run run, User user)
throws ValidationException {
String securityGroups = getSecurityGroups(run, user);
if (securityGroups != null && !securityGroups.isEmpty()) {
launchParams.put("security-groups", securityGroups);
}
}
private Map<String, String> getUserParams(User user)
throws ValidationException {
Map<String, String> userParams = new HashMap<String, String>();
userParams.putAll(getGenericUserParams(user));
userParams.putAll(getConnectorSpecificUserParams(user));
return userParams;
}
protected Map<String, String> getGenericUserParams(User user) {
Map<String, String> userParams = new HashMap<String, String>();
userParams.put(UserParametersFactoryBase.KEY_PARAMETER_NAME, getKey(user));
return userParams;
}
private String getVerbosityLevel(User user) throws ValidationException {
String key = ExecutionControlUserParametersFactory.VERBOSITY_LEVEL;
String qualifiedKey = new ExecutionControlUserParametersFactory().constructKey(key);
String defaultValue = ExecutionControlUserParametersFactory.VERBOSITY_LEVEL_DEFAULT;
return user.getParameterValue(qualifiedKey, defaultValue);
}
protected Map<String, String> getCommandEnvironment(Run run, User user)
throws ConfigurationException, ValidationException {
Map<String, String> environment = getCommonEnvironment(user);
environment.putAll(getContextualizationEnvironment(run, user));
environment.putAll(getCliParamsEnvironment(run, user));
environment.putAll(getConnectorSpecificEnvironment(run, user));
return environment;
}
private Map<String, String> getContextualizationEnvironment(Run run, User user)
throws ConfigurationException, ValidationException {
Configuration configuration = Configuration.getInstance();
String username = user.getName();
String verbosityLevel = getVerbosityLevel(user);
String nodeInstanceName = getInstanceName(run);
Map<String,String> environment = new HashMap<String,String>();
environment.put("SLIPSTREAM_DIID", run.getName());
environment.put("SLIPSTREAM_SERVICEURL", configuration.baseUrl);
environment.put("SLIPSTREAM_NODE_INSTANCE_NAME", nodeInstanceName);
environment.put("SLIPSTREAM_CLOUD", getCloudServiceName());
environment.put("SLIPSTREAM_BUNDLE_URL", configuration.getRequiredProperty("slipstream.update.clienturl"));
environment.put("SLIPSTREAM_BOOTSTRAP_BIN", configuration.getRequiredProperty("slipstream.update.clientbootstrapurl"));
environment.put("SLIPSTREAM_USERNAME", username);
environment.put("SLIPSTREAM_COOKIE", generateCookie(username, run.getUuid()));
environment.put("SLIPSTREAM_VERBOSITY_LEVEL", verbosityLevel);
environment.put("CLOUDCONNECTOR_BUNDLE_URL", getCloudConnectorBundleUrl(user));
environment.put("CLOUDCONNECTOR_PYTHON_MODULENAME", getCloudConnectorPythonModule());
return environment;
}
protected Map<String, String> getCommonEnvironment(User user)
throws ConfigurationException, ValidationException {
Map<String,String> environment = new HashMap<String,String>();
environment.put("SLIPSTREAM_CONNECTOR_INSTANCE", getConnectorInstanceName());
environment.put("__SLIPSTREAM_CLOUD_PASSWORD", getSecret(user));
return environment;
}
private Map<String, String> getCliParamsEnvironment(Run run, User user)
throws ConfigurationException, ValidationException {
String isOrchestrator = (isInOrchestrationContext(run)) ? "True" : "False";
String publicSshKey;
try {
publicSshKey = getPublicSshKey(run, user);
} catch (IOException e) {
throw new ValidationException(e.getMessage());
}
Map<String,String> environment = new HashMap<String,String>();
environment.put("__SLIPSTREAM_SSH_PUB_KEY", publicSshKey);
environment.put("IS_ORCHESTRATOR", isOrchestrator);
return environment;
}
protected String getSecurityGroups(Run run, User user) throws ValidationException {
return (isInOrchestrationContext(run)) ? getOrchestratorSecurityGroups(user) : getImageSecurityGroups(run);
}
protected String getOrchestratorSecurityGroups(User user) throws ValidationException {
return getCloudParameterValue(user, ModuleParametersFactoryBase.SECURITY_GROUPS_PARAMETER_NAME, "");
}
protected String getImageSecurityGroups(Run run) throws ValidationException {
ImageModule machine = ImageModule.load(run.getModuleResourceUrl());
return getParameterValue(ModuleParametersFactoryBase.SECURITY_GROUPS_PARAMETER_NAME, machine);
}
private static void addToPropertiesIfExistAndNotEmpty(Properties properties, String[] parts, int column, String key) {
if (parts.length > column) {
String value = parts[column].trim();
if (!value.isEmpty()) properties.put(key, value);
}
}
public static Map<String, Properties> parseDescribeInstanceResult(String result)
throws SlipStreamException {
Map<String, Properties> instances = new HashMap<String, Properties>();
List<String> lines = new ArrayList<String>();
Collections.addAll(lines, result.split("\n"));
lines.remove(0); // Remove the header line
for (String line : lines) {
Properties properties = new Properties();
String[] parts = line.trim().split(",");
if (parts.length < 2) {
throw (new SlipStreamException("Error returned by describe command. Got: " + result));
}
String instanceId = parts[0].trim();
properties.put(VM_STATE, parts[1].trim());
addToPropertiesIfExistAndNotEmpty(properties, parts, 2, VM_IP);
addToPropertiesIfExistAndNotEmpty(properties, parts, 3, VM_CPU);
addToPropertiesIfExistAndNotEmpty(properties, parts, 4, VM_RAM);
addToPropertiesIfExistAndNotEmpty(properties, parts, 5, VM_DISK);
addToPropertiesIfExistAndNotEmpty(properties, parts, 6, VM_INSTANCE_TYPE);
instances.put(instanceId, properties);
}
return instances;
}
public static String[] parseRunInstanceResult(String result)
throws SlipStreamClientException {
return parseRunInstanceResult(result, Logger.getGlobal());
}
public static String[] parseRunInstanceResult(String result, Logger log)
throws SlipStreamClientException {
String id = null;
String ip = null;
String[] lines = result.split("\n");
for (String line: lines) {
String[] parts = line.trim().split(",");
if (parts.length >= 1 && ! parts[0].trim().isEmpty()) {
id = parts[0];
}
if (parts.length >= 2 && ! parts[1].trim().isEmpty()) {
ip = parts[1];
}
}
if (id == null) {
throw (new SlipStreamClientException("Error returned by launch command. Got: " + result));
}
if (ip == null) {
log.warning("No IP were returned by the launch command");
}
String[] id_ip = {id, ip};
return id_ip;
}
protected String wrapInSingleQuotesOrNull(String value) {
if (value == null || value.isEmpty()) {
return null;
} else {
return wrapInSingleQuotes(value);
}
}
protected String wrapInSingleQuotes(String value) {
return "'" + value.replaceAll("'","'\\\\''") + "'";
}
protected String getNetwork(Run run) throws ValidationException {
if (isInOrchestrationContext(run)) {
return "Public";
} else {
ImageModule machine = ImageModule.load(run.getModuleResourceUrl());
return machine.getParameterValue(ImageModule.NETWORK_KEY, null);
}
}
protected String getErrorMessageLastPart(User user) {
return ", please edit your <a href='/user/" + user.getName()
+ "'> user account</a>";
}
protected void validateCredentials(User user) throws ValidationException {
String errorMessageLastPart = getErrorMessageLastPart(user);
if (getKey(user) == null || getKey(user).isEmpty()) {
throw (new ValidationException(
"Cloud Username cannot be empty"
+ errorMessageLastPart));
}
if (getSecret(user) == null || getSecret(user).isEmpty()) {
throw (new ValidationException(
"Cloud Password cannot be empty"
+ errorMessageLastPart));
}
}
public String getCliLocation() {
return CLI_LOCATION;
}
protected String getCommandGenericPart(){
return getCliLocation() + "/" + getCloudServiceName();
}
protected String getCommandRunInstances() {
return getCommandGenericPart() + "-run-instances ";
}
protected String getCommandTerminateInstances() {
return getCommandGenericPart() + "-terminate-instances ";
}
protected String getCommandDescribeInstances() {
return getCommandGenericPart() + "-describe-instances ";
}
}