/* * Copyright (c) 2013 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 controllers; import static utils.RestUtils.OK_STATUS; import java.io.File; import java.io.FileFilter; import java.util.*; import java.util.concurrent.TimeUnit; import cloudify.widget.api.clouds.CloudProvider; import cloudify.widget.api.clouds.CloudServer; import cloudify.widget.api.clouds.CloudServerApi; import models.ServerNode; import models.User; import models.Widget; import models.WidgetInstanceUserDetails; import org.apache.commons.lang.NumberUtils; import org.codehaus.jackson.JsonNode; import org.jasypt.util.text.BasicTextEncryptor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import play.Play; import play.Routes; import play.i18n.Messages; import play.libs.Akka; import play.libs.F; import play.libs.Json; import play.libs.WS; import play.mvc.Controller; import play.mvc.Http.RequestBody; import play.mvc.Result; import server.ApplicationContext; import server.HeaderMessage; import server.exceptions.ServerException; import utils.CollectionUtils; import utils.RestUtils; import utils.StringUtils; import akka.util.Duration; import com.avaje.ebean.Ebean; import com.avaje.ebean.EbeanServer; import com.avaje.ebean.config.ServerConfig; import com.avaje.ebean.config.dbplatform.MySqlPlatform; import com.avaje.ebeaninternal.api.SpiEbeanServer; import com.avaje.ebeaninternal.server.ddl.DdlGenerator; /** * Widget controller with the main functions like start(), stop(), getWidgetStatus(). * * @author Igor Goldenberg */ public class Application extends Controller { private static Logger logger = LoggerFactory.getLogger(Application.class); /*@Inject private static Conf conf; @Value("${my.name}") private Conf privateConf; @PostConstruct public void init(){ conf = privateConf; } */ public static Result getConf( String name ){ JsonNode json = Json.toJson( ApplicationContext.get().conf().uiConf ); String jsVarResult ="var myConf=" + json.toString() + ";"; return ok( jsVarResult ); } // guy - todo - apiKey should be an encoded string that contains the userId and widgetId. // we should be able to decode it, verify user's ownership on the widget and go from there. /** * Start does 2 things: * - gets a server node * - if remote bootstrap we need to bootstrap one from scratch * - otherwise we take one from the pool. * * - installs the widget recipe * @param apiKey - user api key // * @param project - the HP project API data // * @param key - the HP key API data // * @param secretKey - the HP secret key API data // * @param userId - in case required login authentication is used * @return - result */ public static Result start( String apiKey/*, String project, String key, String secretKey*/, String userId ) { try { //! logger.info( "starting widget with [apiKey, key, secretKey] = [{},{},{}]", new Object[]{apiKey, key, secretKey} ); Widget widget = Widget.getWidget( apiKey ); ServerNode serverNode = null; if ( widget == null || !widget.isEnabled()) { new HeaderMessage().setError( Messages.get("widget.disabled.by.administrator") ).apply( response().getHeaders() ); return badRequest( ); } if ( !StringUtils.isEmptyOrSpaces( widget.getLoginVerificationUrl() ) ) { try { F.Promise<WS.Response> post = WS.url( widget.getLoginVerificationUrl().replace( "$userId", userId ) ).post( "content" ); WS.Response response = post.get( 5L, TimeUnit.SECONDS ); if ( response.getStatus() != 200 ) { return badRequest( "userId not verified : " + response.toString() ); } } catch ( Exception e ) { logger.error( "error while validating userId [{}] on url [{}]", userId, widget.getLoginVerificationUrl() ); } } // credentials validation is made when we attempt to create a PEM file. if credentials are wrong, it will fail. RequestBody requestBody = request().body(); JsonNode advancedData = null; JsonNode recipeProperties = null; if ( requestBody != null && requestBody.asJson() != null && !StringUtils.isEmptyOrSpaces( requestBody.asJson().toString() ) ){ JsonNode jsonNode = requestBody.asJson(); String ADVANCED_DATA_JSON_KEY = "advancedData"; if ( jsonNode.has(ADVANCED_DATA_JSON_KEY) && !StringUtils.isEmptyOrSpaces( jsonNode.get(ADVANCED_DATA_JSON_KEY).toString())){ advancedData = jsonNode.get(ADVANCED_DATA_JSON_KEY); } String RECIPE_PROPERTIES_JSON_KEY = "recipeProperties"; if ( jsonNode.has(RECIPE_PROPERTIES_JSON_KEY) && !StringUtils.isEmptyOrSpaces( jsonNode.get(RECIPE_PROPERTIES_JSON_KEY).toString())){ recipeProperties = jsonNode.get(RECIPE_PROPERTIES_JSON_KEY); } } if ( advancedData != null ){ serverNode = new ServerNode(); serverNode.setAdvancedParams( advancedData.toString() ); serverNode.setRemote(true); serverNode.setWidget(widget); serverNode.save(); }else{ serverNode = ApplicationContext.get().getServerPool().get(); logger.info("application will check if server node is null. if null, there are no available servers"); if (serverNode == null) { ApplicationContext.get().getMailSender().sendPoolIsEmptyMail( ApplicationContext.get().getServerPool().getStats().toString() ); throw new ServerException("i18n:noAvailableServers"); } logger.info("it seems server node is not null. deployment continues as planned"); } if ( recipeProperties != null ){ serverNode.setRecipeProperties( recipeProperties.toString() ); serverNode.save(); } try { logger.info("trying to save user details on server node"); String widgetInstanceUserDetailsStr = session().get(WidgetInstanceUserDetails.COOKIE_NAME); if ( !StringUtils.isEmptyOrSpaces(widgetInstanceUserDetailsStr) && !StringUtils.isEmptyOrSpaces(widget.loginsString) ) { logger.info("I got a cookie"); WidgetInstanceUserDetails widgetInstanceUserDetails = Json.fromJson(Json.parse( widgetInstanceUserDetailsStr ), WidgetInstanceUserDetails.class); widgetInstanceUserDetails.save(); serverNode.widgetInstanceUserDetails = widgetInstanceUserDetails; serverNode.save(); } }catch(Exception e){ logger.error("unable to save widget instance user details",e); } // run the "bootstrap" and "deploy" in another thread. final ServerNode finalServerNode = serverNode; final Widget finalWidget = widget; final String remoteAddress = request().remoteAddress(); logger.info("scheduling deployment"); // TODO : this is a quick fix for the thread exhaustion problem. We need to figure out the best course of action here // TODO : we assume that recipes without URL are fast and so simply don't need a thread for it.. // TODO : however the design should be ignorant to the recipeURL nullability and still scale. if ( StringUtils.isEmptyOrSpaces(widget.getRecipeURL()) ){ logger.info("no recipe url. this should be quick. no need for thread"); logger.info("installing widget on cloud"); ApplicationContext.get().getWidgetServer().deploy(finalWidget, finalServerNode, remoteAddress); }else{ logger.info("recipe url exists. will schedule Akka"); Akka.system().scheduler().scheduleOnce( Duration.create(0, TimeUnit.SECONDS), new Runnable() { @Override public void run() { logger.info("deployment thread started"); if (finalServerNode.isRemote()) { logger.info("trying to find existing management"); try { String ip = getExistingManagement(finalWidget, finalServerNode.advancedParams); if ( !StringUtils.isEmptyOrSpaces(ip )) { logger.info("found management on ip [{}]", ip); finalServerNode.setPublicIP(ip); finalServerNode.save(); }else{ logger.info("did not find management"); } }catch(Exception e){ logger.info("got exception. will try to bootstrap cloud",e); } if ( StringUtils.isEmptyOrSpaces( finalServerNode.getPublicIP() )) { logger.info("bootstrapping remote cloud"); try { if (ApplicationContext.get().getServerBootstrapper().bootstrapCloud(finalServerNode) == null) { logger.info("bootstrap cloud returned NULL. stopping progress."); return; } } catch (Exception e) { logger.error("unable to bootstrap machine", e); return; } }else{ logger.info("skipping bootstrap"); } } logger.info("installing widget on cloud"); ApplicationContext.get().getWidgetServer().deploy(finalWidget, finalServerNode, remoteAddress); } }); } return statusToResult( new Widget.Status().setInstanceId(serverNode.getId().toString()).setRemote(serverNode.isRemote()) ); }catch(ServerException ex) { return exceptionToStatus( ex ); } } public static Result getInjectScript( String publicIp, String privateIp ){ if ( Play.isDev() ){ String injectedScript = ApplicationContext.get().getServerBootstrapper().getInjectedBootstrapScript( publicIp, privateIp ); return ok(injectedScript); }else{ return internalServerError("only available in dev mode"); } } public static Result getPoolStatus( ){ String authToken = session("authToken"); if ( User.validateAuthToken( authToken ) == null ) { return unauthorized(); } return ok(Json.toJson(ApplicationContext.get().getServerPool().getStats())); } // find existing management and returns IP public static String getExistingManagement( Widget widget, String advancedData ){ try { if (StringUtils.isEmptyOrSpaces(widget.managerPrefix)) { return null; } logger.info("searching for existing management using managerPrefix : " + widget.managerPrefix ); CloudServerApi cloudServerApi = ApplicationContext.get().getServerApiFactory().advancedParamsToServerApi( widget.cloudProvider, advancedData ); Collection<CloudServer> allMachinesWithTag = cloudServerApi.getAllMachinesWithTag(""); logger.info("found machines [{}]", CollectionUtils.size(allMachinesWithTag)); for (CloudServer cloudServer : allMachinesWithTag) { logger.info("checking [{}] vs. [{}]", cloudServer.getName(), widget.managerPrefix ); if (cloudServer.getName().startsWith(widget.managerPrefix)) { return cloudServer.getServerIp().publicIp; } } }catch(Exception e){ logger.error("unable to find existing management",e); return null; } return null; } public static Result tearDownRemoteBootstrap( final String widgetApiKey ){ Akka.system().scheduler().scheduleOnce( Duration.create( 0, TimeUnit.SECONDS ), new Runnable() { @Override public void run() { Widget widget = Widget.getWidget(widgetApiKey); if (StringUtils.isEmptyOrSpaces(widget.managerPrefix)) { logger.error("This widget is not configured for remote teardown. please contact admin."); return; } logger.info("tearing down remote bootatrap"); CloudServerApi cloudServerApi = ApplicationContext.get().getCloudServerApi(); // credentials validation is made when we attempt to create a PEM file. if credentials are wrong, it will fail. RequestBody requestBody = request().body(); JsonNode advancedData = null; JsonNode recipeProperties = null; if (requestBody != null && requestBody.asJson() != null && !StringUtils.isEmptyOrSpaces(requestBody.asJson().toString())) { JsonNode jsonNode = requestBody.asJson(); String ADVANCED_DATA_JSON_KEY = "advancedData"; if (jsonNode.has(ADVANCED_DATA_JSON_KEY) && !StringUtils.isEmptyOrSpaces(jsonNode.get(ADVANCED_DATA_JSON_KEY).toString())) { advancedData = jsonNode.get(ADVANCED_DATA_JSON_KEY); } } String managerIp = null; cloudServerApi.connect(); Collection<CloudServer> allMachinesWithTag = cloudServerApi.getAllMachinesWithTag(null); for (CloudServer cloudServer : allMachinesWithTag) { if (cloudServer.getName().startsWith(widget.managerPrefix)) { managerIp = cloudServer.getServerIp().publicIp; } } // ApplicationContext.get().getServerBootstrapper(). if (managerIp == null) { logger.info("did not find a manager to tear down"); } } } ); return ok("TBD"); } private static Result exceptionToStatus( Exception e ){ Widget.Status status = new Widget.Status(); status.setState(Widget.Status.State.STOPPED); status.setMessage(e.getMessage()); return statusToResult(status); } public static Result downloadPemFile( String instanceId ){ ServerNode serverNode = ServerNode.find.byId( Long.parseLong( instanceId ) ); if ( serverNode != null && !StringUtils.isEmpty(serverNode.getPrivateKey()) ){ response().setHeader("Content-Disposition", String.format("attachment; filename=privateKey_%s.pem", instanceId)); return ok ( serverNode.getPrivateKey() ); } return badRequest("instance stopped"); } private static Result statusToResult( Widget.Status status ){ Map<String,Object> result = new HashMap<String, Object>(); result.put("status", status ); logger.debug( "~~~ status=" + status ); logger.debug("statusToResult > result: [{}]", result); return ok( Json.toJson( result )); } public static Result stopPoolInstance( final String apiKey, final String instanceId ) { Akka.system().scheduler().scheduleOnce( Duration.create( 0, TimeUnit.SECONDS ), new Runnable() { @Override public void run() { logger.info( "uninstalling [{}], [{}]", apiKey, instanceId ); Widget widget = Widget.getWidget( apiKey ); if ( instanceId != null ) { logger.info("stopping server node for widget [{}] and instanceId [{}]", widget, instanceId ); ServerNode serverNode = ServerNode.findByWidgetAndInstanceId(widget, instanceId); if ( serverNode != null ) { ApplicationContext.get().getServerBootstrapper().destroyServer( serverNode ); }else{ logger.info("serverNode for widget [{}] and instanceId [{}] does not exit", widget, instanceId ); } // ApplicationContext.get().getWidgetServer().uninstall( serverNode ); // Utils.deleteCachedOutput( serverNode ); } } } ); return ok( OK_STATUS ).as( "application/json" ); } public static Result getWidgetStatus( String apiKey, String instanceId ) { try { logger.debug( "getting status for instance [{}]", instanceId ); if (!NumberUtils.isNumber( instanceId )){ return badRequest(); } ServerNode serverNode = ServerNode.find.byId( Long.parseLong(instanceId) ); Widget.Status wstatus = ApplicationContext.get().getWidgetServer().getWidgetStatus(serverNode); return statusToResult(wstatus); }catch(ServerException ex) { return exceptionToStatus( ex ); } } public static Result generateDDL(){ if ( Play.isDev() ) { EbeanServer defaultServer = Ebean.getServer( "default" ); ServerConfig config = new ServerConfig(); config.setDebugSql( true ); DdlGenerator ddlGenerator = new DdlGenerator( ( SpiEbeanServer ) defaultServer, new MySqlPlatform(), config ); String createDdl = ddlGenerator.generateCreateDdl(); String dropDdl = ddlGenerator.generateDropDdl(); return ok( createDdl ); }else{ return forbidden( ); } } public static Result encrypt(String data) { BasicTextEncryptor textEncryptor = new BasicTextEncryptor(); textEncryptor.setPassword(ApplicationContext.get().conf().applicationSecret); return ok(textEncryptor.encrypt(data)); } public static Result decrypt(String data) { BasicTextEncryptor textEncryptor = new BasicTextEncryptor(); textEncryptor.setPassword(ApplicationContext.get().conf().applicationSecret); return ok(textEncryptor.decrypt(data)); } // public static Result javascriptRoutes() // { // response().setContentType( "text/javascript" ); // return ok( // Routes.javascriptRouter( "jsRoutes", // // Routes for Projects // routes.javascript.WidgetAdmin.getAllWidgets(), // routes.javascript.WidgetAdmin.postWidget(), // routes.javascript.WidgetAdmin.checkPasswordStrength(), // routes.javascript.WidgetAdmin.postChangePassword(), // routes.javascript.WidgetAdmin.getPasswordMatch(), // routes.javascript.WidgetAdmin.postWidgetDescription(), // routes.javascript.WidgetAdmin.deleteWidget(), // routes.javascript.WidgetAdmin.postRequireLogin(), // routes.javascript.WidgetAdmin.regenerateWidgetApiKey(), // routes.javascript.WidgetAdmin.enableWidget(), // routes.javascript.WidgetAdmin.disableWidget(), // // routes.javascript.Application.downloadPemFile(), // routes.javascript.Application.encrypt(), // routes.javascript.Application.decrypt(), // // routes.javascript.AdminPoolController.addNode(), // routes.javascript.AdminPoolController.poolEvents(), // routes.javascript.AdminPoolController.removeNode(), // routes.javascript.AdminPoolController.checkAvailability(), // routes.javascript.AdminPoolController.summary(), // routes.javascript.AdminPoolController.getCloudServers(), // routes.javascript.AdminPoolController.getServerNodes(), // routes.javascript.AdminPoolController.getWidgetInstances(), // routes.javascript.AdminPoolController.getStatuses(), // // // routes.javascript.DemosController.listWidgetForDemoUser() // // ) // ); // } public static Result getCloudProviders() { return ok (Json.toJson(CloudProvider.values())); } public static Result getCloudNames( ){ String cloudifyHome = ApplicationContext.get().conf().server.environment.cloudifyHome; File file = new File(cloudifyHome); logger.info("will find cloud providers for home dir [{}] ", file.getAbsolutePath()); File cloudsFolder = new File(file,"clouds"); File[] cloudProviders = cloudsFolder.listFiles(new FileFilter() { @Override public boolean accept(File pathname) { return pathname.isDirectory(); } }); List<String> result = new LinkedList<String>(); for (File cloudProvider : cloudProviders) { result.add(cloudProvider.getName()); } Collections.sort(result); return ok(Json.toJson(result)); } public static Result logout(){ session().clear(); response().discardCookies( "authToken" ); return ok(); } public static Result login( ){ JsonNode jsonNode = request().body().asJson(); String email = jsonNode.get("email").getTextValue(); String password = jsonNode.get("password").getTextValue(); User authenticated = User.authenticate(email, password); session("authToken", authenticated.getAuthToken()); return ok(); } public static Result isLoggedIn(){ Map<String, Boolean> result = new HashMap<String, Boolean>(); result.put("loggedIn", Boolean.FALSE); try { String authToken = session("authToken"); User user = User.validateAuthToken(authToken); if (user != null) { result.put("loggedIn", Boolean.TRUE); } return ok(Json.toJson(result)); }catch(Exception e){ } return ok(Json.toJson(result)); } public static Result getUserDetails(){ String authToken = session("authToken"); User user = User.validateAuthToken(authToken); return ok(Json.toJson( user )); } }