/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package se.kth.karamel.backend.running.model.tasks;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonIOException;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;
import com.google.gson.stream.JsonReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.log4j.Logger;
import se.kth.karamel.backend.converter.ShellCommandBuilder;
import se.kth.karamel.backend.dag.DagParams;
import se.kth.karamel.backend.machines.MachineInterface;
import se.kth.karamel.backend.machines.TaskSubmitter;
import se.kth.karamel.backend.running.model.MachineRuntime;
import se.kth.karamel.common.cookbookmeta.KaramelizedCookbook;
import se.kth.karamel.common.stats.ClusterStats;
import se.kth.karamel.common.util.Settings;
import se.kth.karamel.common.exception.KaramelException;
/**
*
* @author kamal
*/
public class RunRecipeTask extends Task {
private static final Logger logger = Logger.getLogger(RunRecipeTask.class);
private final String recipeCanonicalName;
private String json;
private final String cookbookId;
private final String cookbookName;
private final List<KaramelizedCookbook> rookCookbooks;
public RunRecipeTask(MachineRuntime machine, ClusterStats clusterStats, String recipe, String json,
TaskSubmitter submitter, String cookbookId, String cookbookName, List<KaramelizedCookbook> rookCookbooks) {
super(recipe, cookbookId + "/" + recipe, false, machine, clusterStats, submitter);
this.recipeCanonicalName = recipe;
this.json = json;
this.cookbookId = cookbookId;
this.cookbookName = cookbookName;
this.rookCookbooks = rookCookbooks;
}
/**
* Recursive method to merge two json objects. Scales O(N^2) - only viable for small json objects.
*
* @param origObj
* @param paramObj
*/
JsonObject merge(JsonObject origObj, JsonObject paramObj) {
Set<Map.Entry<String, JsonElement>> original = origObj.entrySet();
for (Map.Entry<String, JsonElement> entry : paramObj.entrySet()) {
boolean exists = false;
String pKey = entry.getKey();
JsonElement pValue = entry.getValue();
for (Map.Entry<String, JsonElement> o : original) {
if (o.getKey().compareToIgnoreCase(pKey) == 0) {
if (o.getValue().isJsonObject() && pValue.isJsonObject()) {
merge(o.getValue().getAsJsonObject(), pValue.getAsJsonObject());
exists = true;
}
}
}
if (exists == false) {
origObj.add(pKey, pValue);
}
}
return origObj;
}
@Override
public List<ShellCommand> getCommands() throws IOException {
Set<JsonElement> paramsToMerge = DagParams.getGlobalParams();
if (paramsToMerge != null) {
try {
// Merge in Global return results into the json file.
JsonElement obj = new JsonParser().parse(json);
if (obj.isJsonObject()) {
JsonObject jsonObj = obj.getAsJsonObject();
for (JsonElement param : paramsToMerge) {
if (param.isJsonObject()) {
JsonObject paramObj = param.getAsJsonObject();
merge(jsonObj, paramObj);
}
}
GsonBuilder builder = new GsonBuilder();
builder.disableHtmlEscaping();
Gson gson = builder.setPrettyPrinting().create();
json = gson.toJson(jsonObj);
} else {
logger.warn(String.format("Invalid json object for chef-solo: \n %s'", json));
}
} catch (JsonIOException | JsonSyntaxException ex) {
logger.warn(String.format("Invalid return value as Json object: %s \n %s'", ex.toString(), json));
}
}
if (commands == null) {
String jsonFileName = recipeCanonicalName.replaceAll(Settings.COOKBOOK_DELIMITER, "__");
commands = ShellCommandBuilder.makeSingleFileCommand(Settings.SCRIPT_PATH_RUN_RECIPE,
"chef_json", json,
"json_file_name", jsonFileName,
"log_file_name", jsonFileName,
"sudo_command", getSudoCommand(),
"task_id", getId(),
"install_dir_path", Settings.REMOTE_INSTALL_DIR_PATH(getSshUser()),
"succeedtasks_filepath", Settings.SUCCEED_TASKLIST_FILENAME,
"pid_file", Settings.PID_FILE_NAME);
}
return commands;
}
public String getRecipeCanonicalName() {
return recipeCanonicalName;
}
public String getCookbookId() {
return cookbookId;
}
public String getRecipeName() {
return recipeCanonicalName.split(Settings.COOKBOOK_DELIMITER)[1];
}
public String getCookbookName() {
return cookbookName;
}
public static String installRecipeIdFromCookbookName(String machineId, String cookbook) {
String installName = cookbook + Settings.COOKBOOK_DELIMITER + Settings.INSTALL_RECIPE;
return makeUniqueId(machineId, installName);
}
public static String installRecipeIdFromAnotherRecipeName(String machineId, String recipe) {
String[] cmp = recipe.split(Settings.COOKBOOK_DELIMITER);
String installName = cmp[0] + Settings.COOKBOOK_DELIMITER + Settings.INSTALL_RECIPE;
return makeUniqueId(machineId, installName);
}
public static String purgeRecipeIdFromAnotherRecipeName(String machineId, String recipe) {
String[] cmp = recipe.split(Settings.COOKBOOK_DELIMITER);
String purgeName = cmp[0] + Settings.COOKBOOK_DELIMITER + Settings.PURGE_RECIPE;
return makeUniqueId(machineId, purgeName);
}
public static String makeUniqueId(String machineId, String recipe) {
return recipe + " on " + machineId;
}
@Override
public String uniqueId() {
return makeUniqueId(super.getMachineId(), recipeCanonicalName);
}
@Override
public Set<String> dagDependencies() {
Set<String> deps = new HashSet<>();
String installId = installRecipeIdFromAnotherRecipeName(getMachineId(), recipeCanonicalName);
String purgeId = purgeRecipeIdFromAnotherRecipeName(getMachineId(), recipeCanonicalName);
if (uniqueId().equals(installId) || uniqueId().equals(purgeId)) {
for (KaramelizedCookbook kcb : rookCookbooks) {
String id = VendorCookbookTask.makeUniqueId(getMachineId(), kcb.getUrls().id);
deps.add(id);
}
} else {
deps.add(installId);
}
return deps;
}
/**
* It then parses the JSON object and updates a central location for Chef Attributes.
*
* @param sshMachine
* @throws se.kth.karamel.common.exception.KaramelException
*/
@Override
public void collectResults(MachineInterface sshMachine) throws KaramelException {
String remoteFile = Settings.RECIPE_RESULT_REMOTE_PATH(getRecipeCanonicalName());
String localResultsFile = Settings.RECIPE_RESULT_LOCAL_PATH(getRecipeCanonicalName(),
getMachine().getGroup().getCluster().getName(), getMachine().getPublicIp());
try {
sshMachine.downloadRemoteFile(remoteFile, localResultsFile, true);
} catch (IOException ex) {
logger.debug(String.format("No return values for %s on %s", getRecipeCanonicalName(),
getMachine().getPublicIp()));
return;
}
JsonReader reader;
try {
reader = new JsonReader(new FileReader(localResultsFile));
} catch (FileNotFoundException ex) {
String msg = String.format("Cannot find the results file for %s on %s", getRecipeCanonicalName(),
getMachine().getPublicIp());
throw new KaramelException(msg, ex);
}
JsonParser jsonParser = new JsonParser();
try {
JsonElement el = jsonParser.parse(reader);
DagParams.setGlobalParams(el);
} catch (JsonIOException | JsonSyntaxException ex) {
throw new KaramelException(String.format("Invalid return value as Json object for %s on %s",
getRecipeCanonicalName(), getMachine().getPublicIp()), ex);
}
}
@Override
public void downloadExperimentResults(MachineInterface sshMachine) throws KaramelException {
String remoteFile = Settings.EXPERIMENT_RESULT_REMOTE_PATH(getRecipeCanonicalName()) + ".out";
String localResultsFile = Settings.EXPERIMENT_RESULT_LOCAL_PATH(getRecipeCanonicalName(),
getMachine().getGroup().getCluster().getName(), getMachine().getPublicIp());
try {
sshMachine.downloadRemoteFile(remoteFile, localResultsFile, true);
} catch (IOException ex) {
logger.debug(String.format("Cannot find experiment results for download for %s on %s", getRecipeCanonicalName(),
getMachine().getPublicIp()));
return;
}
}
}