package se.kth.karamel.webservice;
import icons.TrayUI;
import io.dropwizard.Application;
import io.dropwizard.assets.AssetsBundle;
import io.dropwizard.jetty.MutableServletContextHandler;
import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;
import java.awt.Desktop;
import java.awt.Image;
import java.awt.SystemTray;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.EnumSet;
import javax.servlet.DispatcherType;
import javax.servlet.FilterRegistration;
import javax.swing.ImageIcon;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.eclipse.jetty.server.AbstractNetworkConnector;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlets.CrossOriginFilter;
import se.kth.karamel.backend.ClusterDefinitionService;
import se.kth.karamel.backend.ClusterManager;
import se.kth.karamel.client.api.KaramelApi;
import se.kth.karamel.client.api.KaramelApiImpl;
import se.kth.karamel.common.CookbookScaffolder;
import static se.kth.karamel.common.CookbookScaffolder.deleteRecursive;
import se.kth.karamel.common.clusterdef.yaml.YamlCluster;
import se.kth.karamel.common.exception.KaramelException;
import se.kth.karamel.common.util.SshKeyPair;
import se.kth.karamel.webservice.calls.cluster.ProcessCommand;
import se.kth.karamel.webservice.calls.cluster.StartCluster;
import se.kth.karamel.webservice.calls.definition.FetchCookbook;
import se.kth.karamel.webservice.calls.definition.JsonToYaml;
import se.kth.karamel.webservice.calls.definition.YamlToJson;
import se.kth.karamel.webservice.calls.ec2.LoadEc2Credentials;
import se.kth.karamel.webservice.calls.ec2.ValidateEc2Credentials;
import se.kth.karamel.webservice.calls.experiment.LoadExperiment;
import se.kth.karamel.webservice.calls.experiment.PushExperiment;
import se.kth.karamel.webservice.calls.experiment.RemoveFileFromExperiment;
import se.kth.karamel.webservice.calls.gce.LoadGceCredentials;
import se.kth.karamel.webservice.calls.gce.ValidateGceCredentials;
import se.kth.karamel.webservice.calls.github.GetGithubCredentials;
import se.kth.karamel.webservice.calls.github.GetGithubOrgs;
import se.kth.karamel.webservice.calls.github.GetGithubRepos;
import se.kth.karamel.webservice.calls.github.RemoveRepository;
import se.kth.karamel.webservice.calls.github.SetGithubCredentials;
import se.kth.karamel.webservice.calls.nova.LoadNovaCredentials;
import se.kth.karamel.webservice.calls.nova.ValidateNovaCredentials;
import se.kth.karamel.webservice.calls.occi.LoadOcciCredentials;
import se.kth.karamel.webservice.calls.occi.ValidateOcciCredentials;
import se.kth.karamel.webservice.calls.sshkeys.GenerateSshKeys;
import se.kth.karamel.webservice.calls.sshkeys.LoadSshKeys;
import se.kth.karamel.webservice.calls.sshkeys.RegisterSshKeys;
import se.kth.karamel.webservice.calls.sshkeys.SetSudoPassword;
import se.kth.karamel.webservice.calls.system.ExitKaramel;
import se.kth.karamel.webservice.calls.system.PingServer;
import se.kth.karamel.webservice.utils.TemplateHealthCheck;
public class KaramelServiceApplication extends Application<KaramelServiceConfiguration> {
private static final org.apache.log4j.Logger logger = org.apache.log4j.Logger.getLogger(
KaramelServiceApplication.class);
private static KaramelApi karamelApi;
public static TrayUI trayUi;
private TemplateHealthCheck healthCheck;
private static final Options options = new Options();
private static final CommandLineParser parser = new GnuParser();
private static final int PORT = 58931;
private static ServerSocket s;
private static boolean cli = false;
private static boolean headless = false;
private static boolean noSudoPasswd = false;
static {
// Ensure a single instance of the app is running
try {
s = new ServerSocket(PORT, 10, InetAddress.getLocalHost());
} catch (UnknownHostException e) {
// shouldn't happen for localhost
} catch (IOException e) {
// port taken, so app is already running
logger.info("An instance of Karamel is already running. Exiting...");
System.exit(10);
}
options.addOption("help", false, "Print help message.");
options.addOption(OptionBuilder.withArgName("yamlFile")
.hasArg()
.withDescription("Dropwizard configuration in a YAML file")
.create("server"));
options.addOption(OptionBuilder.withArgName("yamlFile")
.hasArg()
.withDescription("Karamel cluster definition in a YAML file")
.create("launch"));
options.addOption("scaffold", false, "Creates scaffolding for a new Chef/Karamel Cookbook.");
options.addOption("headless", false, "Launch Karamel from a headless server (no terminal on the server).");
// options.addOption("passwd", false, "Sudo password");
options.addOption(OptionBuilder.withArgName("sudoPassword")
.hasArg()
.withDescription("Sudo password")
.create("passwd"));
}
public static void create() {
String name = "";
try {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
System.out.print("Enter cookbook name: ");
name = br.readLine();
File cb = new File("cookbooks" + File.separator + name);
if (cb.exists()) {
boolean wiped = false;
while (!wiped) {
System.out.print("Do you wan t to over-write the existing cookbook " + name + "? (y/n) ");
String overwrite = br.readLine();
if (overwrite.compareToIgnoreCase("n") == 0 || overwrite.compareToIgnoreCase("no") == 0) {
logger.info("Not over-writing. Exiting.");
System.exit(0);
}
if (overwrite.compareToIgnoreCase("y") == 0 || overwrite.compareToIgnoreCase("yes") == 0) {
deleteRecursive(cb);
wiped = true;
}
}
}
String pathToCb = CookbookScaffolder.create(name);
logger.info("New Cookbook is now located at: " + pathToCb);
System.exit(0);
} catch (IOException ex) {
logger.error("", ex);
System.exit(-1);
}
}
/**
* Usage instructions
*
* @param exitValue
*/
public static void usage(int exitValue) {
HelpFormatter formatter = new HelpFormatter();
formatter.printHelp("karamel", options);
System.exit(exitValue);
}
public static void main(String[] args) throws Exception {
System.setProperty("java.net.preferIPv4Stack", "true");
String yamlTxt;
// These args are sent to the Dropwizard app (thread)
String[] modifiedArgs = new String[2];
modifiedArgs[0] = "server";
String sudoPasswd = "";
karamelApi = new KaramelApiImpl();
try {
CommandLine line = parser.parse(options, args);
if (line.getOptions().length == 0) {
usage(0);
}
if (line.hasOption("help")) {
usage(0);
}
if (line.hasOption("scaffold")) {
create();
}
if (line.hasOption("server")) {
modifiedArgs[1] = line.getOptionValue("server");
}
if (line.hasOption("launch")) {
cli = true;
headless = true;
}
if (line.hasOption("headless")) {
headless = true;
}
if (line.hasOption("passwd")) {
sudoPasswd = line.getOptionValue("passwd");
} else {
noSudoPasswd = true;
}
if (cli) {
ClusterManager.EXIT_ON_COMPLETION = true;
// if (!noSudoPasswd) {
// Console c = null;
// c = System.console();
// if (c == null) {
// System.err.println("No console available.");
// System.exit(1);
// }
// sudoPasswd = c.readLine("Enter your sudo password (just press 'enter' if you don't have one):");
// }
new KaramelServiceApplication().run(modifiedArgs);
Thread.currentThread().sleep(2000);
// Try to open and read the yaml file.
// Print error msg if invalid file or invalid YAML.
yamlTxt = CookbookScaffolder.readFile(line.getOptionValue("launch"));
YamlCluster cluster = ClusterDefinitionService.yamlToYamlObject(yamlTxt);
String jsonTxt = karamelApi.yamlToJson(yamlTxt);
if (!noSudoPasswd && sudoPasswd.isEmpty() == false) {
karamelApi.registerSudoPassword(sudoPasswd);
}
SshKeyPair pair = karamelApi.loadSshKeysIfExist();
karamelApi.registerSshKeys(pair);
karamelApi.startCluster(jsonTxt);
long ms1 = System.currentTimeMillis();
while (ms1 + 60000000 > System.currentTimeMillis()) {
// String clusterStatus = karamelApi.getClusterStatus(cluster.getName());
// logger.debug(clusterStatus);
Thread.currentThread().sleep(30000);
}
}
} catch (ParseException e) {
usage(-1);
} catch (KaramelException e) {
System.err.println("Inalid yaml file; " + e.getMessage());
System.exit(-2);
}
if (!cli) {
new KaramelServiceApplication().run(modifiedArgs);
}
Runtime.getRuntime().addShutdownHook(new KaramelCleanupBeforeShutdownThread());
}
// Name of the application displayed when application boots up.
@Override
public String getName() {
return "karamel-core";
}
// Pre start of the dropwizard to plugin with separate bundles.
@Override
public void initialize(Bootstrap<KaramelServiceConfiguration> bootstrap) {
logger.debug("Executing any initialization tasks.");
// bootstrap.addBundle(new ConfiguredAssetsBundle("/assets/", "/dashboard/"));
// https://groups.google.com/forum/#!topic/dropwizard-user/UaVcAYm0VlQ
bootstrap.addBundle(new AssetsBundle("/assets/", "/"));
}
@Override
public void run(KaramelServiceConfiguration configuration, Environment environment) throws Exception {
healthCheck = new TemplateHealthCheck("%s");
// http://stackoverflow.com/questions/26610502/serve-static-content-from-a-base-url-in-dropwizard-0-7-1
// environment.jersey().setUrlPattern("/angular/*");
/*
* To allow cross orign resource request from angular js client
*/
FilterRegistration.Dynamic filter = environment.servlets().addFilter("CORS", CrossOriginFilter.class
);
// Allow cross origin requests.
filter.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class
), true, "/*");
filter.setInitParameter(
"allowedOrigins", "*"); // allowed origins comma separated
filter.setInitParameter(
"allowedHeaders", "Content-Type,Authorization,X-Requested-With,Content-Length,Accept,Origin");
filter.setInitParameter(
"allowedMethods", "GET,PUT,POST,DELETE,OPTIONS,HEAD");
filter.setInitParameter(
"preflightMaxAge", "5184000"); // 2 months
filter.setInitParameter(
"allowCredentials", "true");
environment.jersey()
.setUrlPattern("/api/*");
environment.healthChecks()
.register("template", healthCheck);
//definitions
environment.jersey().register(new YamlToJson(karamelApi));
environment.jersey().register(new JsonToYaml(karamelApi));
environment.jersey().register(new FetchCookbook(karamelApi));
//ssh
environment.jersey().register(new LoadSshKeys(karamelApi));
environment.jersey().register(new RegisterSshKeys(karamelApi));
environment.jersey().register(new GenerateSshKeys(karamelApi));
environment.jersey().register(new SetSudoPassword(karamelApi));
//ec2
environment.jersey().register(new LoadEc2Credentials(karamelApi));
environment.jersey().register(new ValidateEc2Credentials(karamelApi));
//gce
environment.jersey().register(new LoadGceCredentials(karamelApi));
environment.jersey().register(new ValidateGceCredentials(karamelApi));
//cluster
environment.jersey().register(new StartCluster(karamelApi));
environment.jersey().register(new ProcessCommand(karamelApi));
environment.jersey().register(new ExitKaramel(karamelApi));
environment.jersey().register(new PingServer(karamelApi));
//github
environment.jersey().register(new GetGithubCredentials(karamelApi));
environment.jersey().register(new SetGithubCredentials(karamelApi));
environment.jersey().register(new GetGithubOrgs(karamelApi));
environment.jersey().register(new GetGithubRepos(karamelApi));
environment.jersey().register(new RemoveRepository(karamelApi));
//experiment
environment.jersey().register(new LoadExperiment(karamelApi));
environment.jersey().register(new PushExperiment(karamelApi));
environment.jersey().register(new RemoveFileFromExperiment(karamelApi));
//Openstack nova
environment.jersey().register(new LoadNovaCredentials(karamelApi));
environment.jersey().register(new ValidateNovaCredentials(karamelApi));
//occi
environment.jersey().register(new LoadOcciCredentials(karamelApi));
environment.jersey().register(new ValidateOcciCredentials(karamelApi));
// Wait to make sure jersey/angularJS is running before launching the browser
final int webPort = getPort(environment);
if (!headless) {
if (SystemTray.isSupported()) {
trayUi = new TrayUI(createImage("if.png", "tray icon"), getPort(environment));
}
new Thread("webpage opening..") {
public void run() {
try {
Thread.sleep(1500);
openWebpage(new URL("http://localhost:" + webPort + "/index.html#/"));
} catch (InterruptedException e) {
// swallow the exception
} catch (java.net.MalformedURLException e) {
// swallow the exception
}
}
}.start();
}
}
protected static Image createImage(String path, String description) {
URL imageURL = TrayUI.class.getResource(path);
if (imageURL == null) {
System.err.println("Resource not found: " + path);
return null;
} else {
return (new ImageIcon(imageURL, description)).getImage();
}
}
public int getPort(Environment environment) {
int defaultPort = 9090;
MutableServletContextHandler h = environment.getApplicationContext();
if (h == null) {
return defaultPort;
}
Server s = h.getServer();
if (s == null) {
return defaultPort;
}
Connector[] c = s.getConnectors();
if (c != null && c.length > 0) {
AbstractNetworkConnector anc = (AbstractNetworkConnector) c[0];
if (anc != null) {
return anc.getLocalPort();
}
}
return defaultPort;
}
public synchronized static void openWebpage(URI uri) {
Desktop desktop = Desktop.isDesktopSupported() ? Desktop.getDesktop() : null;
if (desktop != null && desktop.isSupported(Desktop.Action.BROWSE)) {
try {
desktop.browse(uri);
} catch (Exception e) {
e.printStackTrace();
}
} else {
System.err.println("Brower UI could not be launched using Java's Desktop library. "
+ "Are you running a window manager?");
System.err.println("If you are using Ubuntu, try: sudo apt-get install libgnome");
System.err.println("Retrying to launch the browser now using a different method.");
BareBonesBrowserLaunch.openURL(uri.toASCIIString());
}
}
public static void openWebpage(URL url) {
try {
openWebpage(url.toURI());
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
static class KaramelCleanupBeforeShutdownThread extends Thread {
@Override
public void run() {
logger.info("Bye! Cleaning up first....");
// TODO - interrupt all threads
// Should we cleanup AMIs?
}
}
}