package se.kth.karamel.client.api;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.apache.commons.io.FileUtils;
import org.jclouds.ContextBuilder;
import org.jclouds.domain.Credentials;
import org.jclouds.openstack.nova.v2_0.NovaApiMetadata;
import se.kth.karamel.backend.ClusterDefinitionService;
import se.kth.karamel.backend.ClusterService;
import se.kth.karamel.backend.Experiment;
import se.kth.karamel.backend.command.CommandResponse;
import se.kth.karamel.backend.command.CommandService;
import se.kth.karamel.backend.github.GithubApi;
import se.kth.karamel.backend.github.GithubUser;
import se.kth.karamel.backend.github.OrgItem;
import se.kth.karamel.backend.github.RepoItem;
import se.kth.karamel.backend.github.util.ChefExperimentExtractor;
import se.kth.karamel.backend.github.util.GithubUrl;
import se.kth.karamel.backend.launcher.amazon.Ec2Context;
import se.kth.karamel.backend.launcher.amazon.Ec2Launcher;
import se.kth.karamel.backend.launcher.google.GceContext;
import se.kth.karamel.backend.launcher.google.GceLauncher;
import se.kth.karamel.backend.launcher.nova.NovaContext;
import se.kth.karamel.backend.launcher.nova.NovaLauncher;
import se.kth.karamel.backend.launcher.occi.OcciContext;
import se.kth.karamel.backend.launcher.occi.OcciLauncher;
import se.kth.karamel.backend.running.model.ClusterRuntime;
import se.kth.karamel.backend.running.model.GroupRuntime;
import se.kth.karamel.backend.running.model.MachineRuntime;
import se.kth.karamel.backend.running.model.serializers.ClusterEntitySerializer;
import se.kth.karamel.backend.running.model.serializers.DefaultTaskSerializer;
import se.kth.karamel.backend.running.model.serializers.GroupEntitySerializer;
import se.kth.karamel.backend.running.model.serializers.MachineEntitySerializer;
import se.kth.karamel.backend.running.model.serializers.ShellCommandSerializer;
import se.kth.karamel.backend.running.model.tasks.AptGetEssentialsTask;
import se.kth.karamel.backend.running.model.tasks.InstallChefdkTask;
import se.kth.karamel.backend.running.model.tasks.MakeSoloRbTask;
import se.kth.karamel.backend.running.model.tasks.RunRecipeTask;
import se.kth.karamel.backend.running.model.tasks.ShellCommand;
import se.kth.karamel.backend.running.model.tasks.VendorCookbookTask;
import se.kth.karamel.common.cookbookmeta.Berksfile;
import se.kth.karamel.common.cookbookmeta.DefaultRb;
import se.kth.karamel.common.cookbookmeta.ExperimentRecipe;
import se.kth.karamel.common.cookbookmeta.InstallRecipe;
import se.kth.karamel.common.cookbookmeta.KaramelFile;
import se.kth.karamel.common.cookbookmeta.KaramelFileYamlDeps;
import se.kth.karamel.common.cookbookmeta.KaramelizedCookbook;
import se.kth.karamel.common.exception.InvalidNovaCredentialsException;
import se.kth.karamel.common.exception.InvalidOcciCredentialsException;
import se.kth.karamel.common.exception.KaramelException;
import se.kth.karamel.common.util.Confs;
import se.kth.karamel.common.util.Ec2Credentials;
import se.kth.karamel.common.util.NovaCredentials;
import se.kth.karamel.common.util.OcciCredentials;
import se.kth.karamel.common.util.Settings;
import se.kth.karamel.common.util.SshKeyPair;
import se.kth.karamel.common.util.SshKeyService;
import se.kth.karamel.common.util.settings.NovaSetting;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import se.kth.karamel.common.cookbookmeta.CookbookCache;
/**
* Implementation of the Karamel Api for UI
*
* @author kamal
*/
public class KaramelApiImpl implements KaramelApi {
private static final org.apache.log4j.Logger logger = org.apache.log4j.Logger.getLogger(KaramelApiImpl.class);
private static final ClusterService clusterService = ClusterService.getInstance();
@Override
public String commandCheatSheet() throws KaramelException {
return CommandService.processCommand("help").getResult();
}
@Override
public CommandResponse processCommand(String command, String... args) throws KaramelException {
return CommandService.processCommand(command, args);
}
@Override
public String getCookbookDetails(String cookbookUrl, boolean refresh) throws KaramelException {
Set<String> urls = new HashSet<>();
urls.add(cookbookUrl);
CookbookCache cache = ClusterDefinitionService.CACHE;
if (refresh) {
cache.prepareNewParallel(urls);
KaramelizedCookbook cb = cache.get(cookbookUrl);
return cb.getInfoJson();
} else {
cache.prepareParallel(urls);
KaramelizedCookbook cb = cache.get(cookbookUrl);
return cb.getInfoJson();
}
}
@Override
public String jsonToYaml(String json) throws KaramelException {
return ClusterDefinitionService.jsonToYaml(json);
}
@Override
public String yamlToJson(String yaml) throws KaramelException {
return ClusterDefinitionService.yamlToJson(yaml);
}
@Override
public Ec2Credentials loadEc2CredentialsIfExist() throws KaramelException {
Confs confs = Confs.loadKaramelConfs();
return Ec2Launcher.readCredentials(confs);
}
@Override
public boolean updateEc2CredentialsIfValid(Ec2Credentials credentials) throws KaramelException {
Ec2Context context = Ec2Launcher.validateCredentials(credentials);
Confs confs = Confs.loadKaramelConfs();
confs.put(Settings.AWS_ACCESSKEY_KEY, credentials.getAccessKey());
confs.put(Settings.AWS_SECRETKEY_KEY, credentials.getSecretKey());
confs.writeKaramelConfs();
clusterService.registerEc2Context(context);
return true;
}
@Override
public String loadGceCredentialsIfExist() throws KaramelException {
Confs confs = Confs.loadKaramelConfs();
String path = confs.getProperty(Settings.GCE_JSON_KEY_FILE_PATH);
if (path != null) {
Credentials credentials = GceLauncher.readCredentials(path);
if (credentials != null) {
return path;
}
}
return null;
}
@Override
public boolean updateGceCredentialsIfValid(String jsonFilePath) throws KaramelException {
if (jsonFilePath.isEmpty() || jsonFilePath == null) {
return false;
}
try {
Credentials credentials = GceLauncher.readCredentials(jsonFilePath);
GceContext context = GceLauncher.validateCredentials(credentials);
Confs confs = Confs.loadKaramelConfs();
confs.put(Settings.GCE_JSON_KEY_FILE_PATH, jsonFilePath);
confs.writeKaramelConfs();
clusterService.registerGceContext(context);
} catch (Throwable ex) {
throw new KaramelException(ex.getMessage());
}
return true;
}
@Override
public NovaCredentials loadNovaCredentialsIfExist() throws KaramelException {
Confs confs = Confs.loadKaramelConfs();
return NovaLauncher.readCredentials(confs);
}
@Override
public boolean updateNovaCredentialsIfValid(NovaCredentials credentials) throws InvalidNovaCredentialsException {
NovaContext context = NovaLauncher.validateCredentials(credentials,
ContextBuilder.newBuilder(new NovaApiMetadata()));
Confs confs = Confs.loadKaramelConfs();
confs.put(NovaSetting.NOVA_ACCOUNT_ID_KEY.getParameter(), credentials.getAccountName());
confs.put(NovaSetting.NOVA_ACCESSKEY_KEY.getParameter(), credentials.getAccountPass());
confs.put(NovaSetting.NOVA_ACCOUNT_ENDPOINT.getParameter(), credentials.getEndpoint());
confs.put(NovaSetting.NOVA_REGION.getParameter(), credentials.getRegion());
confs.put(NovaSetting.NOVA_NETWORKID.getParameter(), credentials.getNetworkId());
confs.writeKaramelConfs();
clusterService.registerNovaContext(context);
return true;
}
public OcciCredentials loadOcciCredentialsIfExist() throws KaramelException {
Confs confs = Confs.loadKaramelConfs();
return OcciLauncher.readCredentials(confs);
}
@Override
public boolean updateOcciCredentialsIfValid(OcciCredentials credentials) throws InvalidOcciCredentialsException {
OcciContext context = OcciLauncher.validateCredentials(credentials);
Confs confs = Confs.loadKaramelConfs();
confs.put("occi.user.certificate.path", credentials.getUserCertificatePath());
confs.put("occi.certificate.dir", credentials.getSystemCertDir());
confs.writeKaramelConfs();
clusterService.registerOcciContext(context);
return true;
}
@Override
public String getClusterStatus(String clusterName) throws KaramelException {
ClusterRuntime clusterManager = clusterService.clusterStatus(clusterName);
GsonBuilder builder = new GsonBuilder();
builder.disableHtmlEscaping();
Gson gson = builder.
registerTypeAdapter(ClusterRuntime.class, new ClusterEntitySerializer()).
registerTypeAdapter(MachineRuntime.class, new MachineEntitySerializer()).
registerTypeAdapter(GroupRuntime.class, new GroupEntitySerializer()).
registerTypeAdapter(ShellCommand.class, new ShellCommandSerializer()).
registerTypeAdapter(RunRecipeTask.class, new DefaultTaskSerializer()).
registerTypeAdapter(MakeSoloRbTask.class, new DefaultTaskSerializer()).
registerTypeAdapter(VendorCookbookTask.class, new DefaultTaskSerializer()).
registerTypeAdapter(AptGetEssentialsTask.class, new DefaultTaskSerializer()).
registerTypeAdapter(InstallChefdkTask.class, new DefaultTaskSerializer()).
setPrettyPrinting().
create();
String json = gson.toJson(clusterManager);
return json;
}
@Override
public void pauseCluster(String clusterName) throws KaramelException {
clusterService.pauseDag(clusterName);
}
@Override
public void resumeCluster(String clusterName) throws KaramelException {
clusterService.resumeDag(clusterName);
}
@Override
public void terminateCluster(String clusterName) throws KaramelException {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public void startCluster(String json) throws KaramelException {
logger.info("cluster to launch: \n" + json);
clusterService.startCluster(json);
}
@Override
public String getInstallationDag(String clusterName) throws KaramelException {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public SshKeyPair loadSshKeysIfExist() throws KaramelException {
Confs confs = Confs.loadKaramelConfs();
SshKeyPair sshKeys = SshKeyService.loadSshKeys(confs);
return sshKeys;
}
@Override
public SshKeyPair loadSshKeysIfExist(String clusterName) throws KaramelException {
Confs confs = Confs.loadAllConfsForCluster(clusterName);
SshKeyPair sshKeys = SshKeyService.loadSshKeys(confs);
return sshKeys;
}
@Override
public SshKeyPair generateSshKeysAndUpdateConf() throws KaramelException {
SshKeyPair sshkeys = SshKeyService.generateAndStoreSshKeys();
Confs confs = Confs.loadKaramelConfs();
confs.put(Settings.SSH_PRIVKEY_PATH_KEY, sshkeys.getPrivateKeyPath());
confs.put(Settings.SSH_PUBKEY_PATH_KEY, sshkeys.getPublicKeyPath());
confs.writeKaramelConfs();
return sshkeys;
}
@Override
public SshKeyPair generateSshKeysAndUpdateConf(String clusterName) throws KaramelException {
SshKeyPair sshkeys = SshKeyService.generateAndStoreSshKeys(clusterName);
Confs confs = Confs.loadJustClusterConfs(clusterName);
confs.put(Settings.SSH_PRIVKEY_PATH_KEY, sshkeys.getPrivateKeyPath());
confs.put(Settings.SSH_PUBKEY_PATH_KEY, sshkeys.getPublicKeyPath());
confs.writeClusterConfs(clusterName);
return sshkeys;
}
@Override
public SshKeyPair registerSshKeys(SshKeyPair keypair) throws KaramelException {
Confs confs = Confs.loadKaramelConfs();
saveSshConfs(keypair, confs);
confs.writeKaramelConfs();
keypair = SshKeyService.loadSshKeys(keypair.getPublicKeyPath(), keypair.getPrivateKeyPath(),
keypair.getPassphrase());
clusterService.registerSshKeyPair(keypair);
return keypair;
}
private void saveSshConfs(SshKeyPair keypair, Confs confs) {
confs.put(Settings.SSH_PRIVKEY_PATH_KEY, keypair.getPrivateKeyPath());
confs.put(Settings.SSH_PUBKEY_PATH_KEY, keypair.getPublicKeyPath());
}
@Override
public SshKeyPair registerSshKeys(String clusterName, SshKeyPair keypair) throws KaramelException {
Confs confs = Confs.loadJustClusterConfs(clusterName);
saveSshConfs(keypair, confs);
confs.writeClusterConfs(clusterName);
keypair = SshKeyService.loadSshKeys(keypair.getPublicKeyPath(), keypair.getPrivateKeyPath(),
keypair.getPassphrase());
clusterService.registerSshKeyPair(clusterName, keypair);
return keypair;
}
@Override
public void registerSudoPassword(String password) {
ClusterService.getInstance().getCommonContext().setSudoAccountPassword(password);
}
@Override
public List<OrgItem> listGithubOrganizations() throws KaramelException {
return GithubApi.getOrganizations();
}
@Override
public List<RepoItem> listGithubRepos(String organization) throws KaramelException {
return GithubApi.getRepos(organization);
}
@Override
public GithubUser registerGithubAccount(String user, String password) throws KaramelException {
return GithubApi.registerCredentials(user, password);
}
@Override
public GithubUser loadGithubCredentials() throws KaramelException {
return GithubApi.loadGithubCredentials();
}
private void initGithubRepo(String user, String owner, String repo, String description) throws KaramelException {
if (owner == null || owner.isEmpty() || owner.compareToIgnoreCase(user) == 0) {
GithubApi.createRepoForUser(repo, description);
} else {
GithubApi.createRepoForOrg(owner, repo, description);
}
}
@Override
public void commitAndPushExperiment(Experiment experiment) throws KaramelException {
String owner = experiment.getGithubOwner();
String repoName = experiment.getGithubRepo();
File f = GithubApi.getRepoDirectory(repoName);
boolean repoExists = GithubApi.repoExists(owner, repoName);
if (repoExists) {
// local copy must exist, already pushed to GitHub
if (!f.exists()) {
throw new KaramelException("Remote repository already exists. Load the experiment if it already exists.");
}
} else // no repo on GitHub. Should not exist a local directory with same repo name.
{
if (f.exists()) {
throw new KaramelException("The remote repo does not exist, however a conflicting local directory was found. "
+ "Remove the local directory in ~/.karamel/cookbook_designer first, then save again.");
} else {
// Create the repo if it doesn't exist and clone it to a local directory
// That way, the local directory will only ever exist if it the repo has been created first.
// Users should subsequently load a directory from GitHub.
initGithubRepo(GithubApi.getUser(), owner, repoName, experiment.getDescription());
// Scaffold a new experiment project with Karamel/Chef
GithubApi.scaffoldRepo(repoName);
}
}
// For all config and script files, compile them and generate Karamel/Chef files
ChefExperimentExtractor.parseAttributesAddToGit(owner, repoName, experiment);
ChefExperimentExtractor.parseRecipesAddToGit(owner, repoName, experiment);
// Commit and push all changes to github
GithubApi.commitPush(owner, repoName);
}
@Override
public Experiment loadExperiment(String githubRepoUrl) throws KaramelException {
Experiment ec = new Experiment();
String repoName = GithubUrl.extractRepoName(githubRepoUrl);
ec.setGithubRepo(repoName);
String owner = GithubUrl.extractUserName(githubRepoUrl);
ec.setGithubOwner(owner);
if (repoName == null || owner == null || repoName.isEmpty() || owner.isEmpty()) {
throw new KaramelException("Misformed url repo/owner: " + githubRepoUrl);
}
// Loading a repo involves wiping any existing local copy and replacing it with GitHub's copy.
File localPath = new File(Settings.COOKBOOKS_PATH + File.separator + repoName);
if (localPath.isDirectory() == true) {
try {
FileUtils.deleteDirectory(localPath);
} catch (IOException ex) {
logger.warn(ex.getMessage());
if (localPath.isDirectory() == true) {
throw new KaramelException("Couldn't remove local copy of the repo at directory: " + localPath.getPath());
}
}
}
// Download the latest copy from GitHub
GithubApi.cloneRepo(owner, repoName);
String strippedUrl = githubRepoUrl.replaceAll("\\.git", "");
ec.setUrlGitClone(githubRepoUrl);
KaramelizedCookbook kc = new KaramelizedCookbook(strippedUrl, true);
KaramelFile kf = kc.getKaramelFile();
Berksfile bf = kc.getBerksFile();
DefaultRb attributes = kc.getDefaultRb();
List<ExperimentRecipe> er = kc.getExperimentRecipes();
InstallRecipe ir = kc.getInstallRecipe();
ec.setUser((String) attributes.getValue(repoName + "/user"));
ec.setGroup((String) attributes.getValue(repoName + "/group"));
ec.setUrlBinary((String) attributes.getValue(repoName + "/url"));
ec.setBerksfile(bf.toString());
ec.setExperimentSetupCode(ir.getSetupCode());
ec.setDefaultAttributes(attributes.getExperimentContextFormat());
ArrayList<KaramelFileYamlDeps> deps = kf.getDependencies();
Set<String> localSet = new HashSet<>();
Set<String> globalSet = new HashSet<>();
for (KaramelFileYamlDeps yd : deps) {
if (!yd.getRecipe().contains(Settings.COOKBOOK_DELIMITER + "install")) {
List<String> locals = yd.getLocal();
// remove duplicates from locals
localSet.addAll(locals);
List<String> globals = yd.getGlobal();
globalSet.addAll(globals);
}
}
StringBuilder local = new StringBuilder();
StringBuilder global = new StringBuilder();
int i = 0;
for (String s : localSet) {
if (i == 0) {
local.append(s);
} else {
local.append(System.lineSeparator()).append(s);
}
i++;
}
i = 0;
for (String s : globalSet) {
if (i == 0) {
global.append(s);
} else {
global.append(System.lineSeparator()).append(s);
}
i++;
}
ec.setLocalDependencies(local.toString());
ec.setGlobalDependencies(global.toString());
for (ExperimentRecipe r : er) {
Experiment.Code exp = new Experiment.Code(r.getRecipeName(), r.getScriptContents(), r.getConfigFileName(),
r.getConfigFileContents(), r.getScriptType());
List<Experiment.Code> exps = ec.getCode();
exps.add(exp);
}
return ec;
}
@Override
public RepoItem createGithubRepo(String org, String repo, String description) throws KaramelException {
return GithubApi.createRepoForOrg(org, repo, description);
}
@Override
public void removeFileFromExperiment(String owner, String repo, String experimentName) {
try {
GithubApi.removeFile(owner, repo, experimentName);
} catch (KaramelException ex) {
// Do nothing - Repository hasn't been created yet. That's ok.");
}
}
@Override
public void removeRepo(String owner, String repo, boolean removeLocal, boolean removeGitHub) throws KaramelException {
boolean failedLocal = false;
String failedMsg = "";
// if failure while removing locally, still try and remove remote repo (if requested)
if (removeLocal) {
try {
GithubApi.removeLocalRepo(owner, repo);
} catch (KaramelException ex) {
failedLocal = true;
failedMsg = ex.toString();
}
}
if (removeGitHub) {
GithubApi.removeRepo(owner, repo);
}
if (failedLocal) {
throw new KaramelException(failedMsg);
}
}
}