/* * Copyright (c) 2014. * * BaasBox - info-at-baasbox.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. */ package com.baasbox.controllers; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.math.BigInteger; import java.util.HashMap; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.zip.ZipInputStream; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.exception.ExceptionUtils; import com.baasbox.service.logging.BaasBoxLogger; import play.libs.Json; import play.mvc.Controller; import play.mvc.Http; import play.mvc.Http.MultipartFormData; import play.mvc.Http.MultipartFormData.FilePart; import play.mvc.Result; import play.mvc.With; import com.baasbox.BBConfiguration; import com.baasbox.controllers.actions.filters.ConnectToDBFilter; import com.baasbox.controllers.actions.filters.RootCredentialWrapFilter; import com.baasbox.dao.exception.FileNotFoundException; import com.baasbox.dao.exception.SqlInjectionException; import com.baasbox.exception.OpenTransactionException; import com.baasbox.exception.UserNotFoundException; import com.baasbox.metrics.BaasBoxMetric; import com.baasbox.service.dbmanager.DbManagerService; import com.baasbox.service.user.UserService; import com.codahale.metrics.json.MetricsModule; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; public class Root extends Controller { @With({RootCredentialWrapFilter.class,ConnectToDBFilter.class}) public static Result resetAdminPassword(){ Http.RequestBody body = request().body(); JsonNode bodyJson= body.asJson(); if (BaasBoxLogger.isDebugEnabled()) BaasBoxLogger.debug("resetAdminPassword bodyJson: " + bodyJson); //check and validate input if (bodyJson==null) return badRequest("The body payload cannot be empty."); if (!bodyJson.has("password")) return badRequest("The 'password' field is missing into the body"); JsonNode passwordNode=bodyJson.findValue("password"); if (passwordNode==null) return badRequest("The body payload doesn't contain password field"); String password=passwordNode.asText(); try{ UserService.changePassword("admin", password); } catch (SqlInjectionException e) { return badRequest("The password is not valid"); } catch (UserNotFoundException e) { BaasBoxLogger.error("User 'admin' not found!"); return internalServerError("User 'admin' not found!"); } catch (OpenTransactionException e) { BaasBoxLogger.error (ExceptionUtils.getFullStackTrace(e)); throw new RuntimeException(e); } return ok("Admin password reset"); } @With(RootCredentialWrapFilter.class) public static Result timers() throws JsonProcessingException { if (!BaasBoxMetric.isActivate()) return status(SERVICE_UNAVAILABLE,"The metrics service are disabled"); ObjectMapper mapper = new ObjectMapper().registerModule(new MetricsModule(TimeUnit.SECONDS, TimeUnit.MILLISECONDS, false)); return ok(mapper.writeValueAsString(BaasBoxMetric.registry.getTimers())); } @With(RootCredentialWrapFilter.class) public static Result counters() throws JsonProcessingException { if (!BaasBoxMetric.isActivate()) return status(SERVICE_UNAVAILABLE,"The metrics service are disabled"); ObjectMapper mapper = new ObjectMapper().registerModule(new MetricsModule(TimeUnit.SECONDS, TimeUnit.SECONDS, false)); return ok(mapper.writeValueAsString(BaasBoxMetric.registry.getCounters())); } @With(RootCredentialWrapFilter.class) public static Result meters() throws JsonProcessingException { if (!BaasBoxMetric.isActivate()) return status(SERVICE_UNAVAILABLE,"The metrics service are disabled"); ObjectMapper mapper = new ObjectMapper().registerModule(new MetricsModule(TimeUnit.SECONDS, TimeUnit.SECONDS, false)); return ok(mapper.writeValueAsString(BaasBoxMetric.registry.getMeters())); } @With(RootCredentialWrapFilter.class) public static Result gauges() throws JsonProcessingException { if (!BaasBoxMetric.isActivate()) return status(SERVICE_UNAVAILABLE,"The metrics service are disabled"); ObjectMapper mapper = new ObjectMapper().registerModule(new MetricsModule(TimeUnit.SECONDS, TimeUnit.SECONDS, false)); return ok(mapper.writeValueAsString(BaasBoxMetric.registry.getGauges())); } @With(RootCredentialWrapFilter.class) public static Result histograms() throws JsonProcessingException { if (!BaasBoxMetric.isActivate()) return status(SERVICE_UNAVAILABLE,"The metrics service is disabled"); ObjectMapper mapper = new ObjectMapper().registerModule(new MetricsModule(TimeUnit.SECONDS, TimeUnit.SECONDS, false)); return ok(mapper.writeValueAsString(BaasBoxMetric.registry.getHistograms())); } @With(RootCredentialWrapFilter.class) public static Result uptime() throws JsonProcessingException { ObjectMapper mapper = new ObjectMapper(); HashMap <String,Object> ret = new HashMap<String, Object>(); ret.put("start_time", BaasBoxMetric.Track.getStartTime()); ret.put("time_zone", "UTC"); ret.put("uptime", BaasBoxMetric.Track.getUpTimeinMillis()); ret.put("time_unit", "ms"); return ok(mapper.writeValueAsString(ret)); } @With(RootCredentialWrapFilter.class) public static Result startMetrics() throws JsonProcessingException { BaasBoxMetric.start(); return ok("Metrics service started"); } @With(RootCredentialWrapFilter.class) public static Result stopMetrics() throws JsonProcessingException { BaasBoxMetric.stop(); return ok("Metrics service stopped"); } //backup & restore /** * /root/db/export (POST) * * the method generate a full dump of the db in an asyncronus task. * the response returns a 202 code (ACCEPTED) and the filename of the * file that will be generated. * * The async nature of the method DOES NOT ensure the creation of the file * so, querying for the file name with the /admin/db/:filename could return a 404 * @return a 202 accepted code and a json representation containing the filename of the generated file */ @With({RootCredentialWrapFilter.class,ConnectToDBFilter.class}) public static Result exportDb(){ String appcode = (String)ctx().args.get("appcode"); String fileName=""; try { fileName = DbManagerService.exportDb(appcode); } catch (FileNotFoundException e) { return internalServerError(ExceptionUtils.getMessage(e)); } return status(202,Json.toJson(fileName)); } /** * /root/db/export/:filename (GET) * * the method returns the stream of the export file named after :filename parameter. * * if the file is not present a 404 code is returned to the client * * @return a 200 ok code and the stream of the file */ @With({RootCredentialWrapFilter.class,ConnectToDBFilter.class}) public static Result getExport(String filename){ java.io.File file = new java.io.File(DbManagerService.backupDir+DbManagerService.fileSeparator+filename); if(!file.exists()){ return notFound(); }else{ response().setContentType("application/zip"); //added in Play 2.2.1. it is very strange because the content type should be set by the framework return ok(file); } } /** * /root/db/export/:filename (DELETE) * * Deletes an export file from the db backup folder, if it exists * * * @param fileName the name of the file to be deleted * @return a 200 code if the file is deleted correctly or a 404.If the file could not * be deleted a 500 error code is returned */ @With({RootCredentialWrapFilter.class,ConnectToDBFilter.class}) public static Result deleteExport(String filename){ try { DbManagerService.deleteExport(filename); } catch (FileNotFoundException e1) { return notFound(); } catch (IOException e1) { return internalServerError("Unable to delete export.It will be deleted on the next reboot."+filename); } return ok(); } /** * /root/db/export (GET) * * the method returns the list as a json array of all the export files * stored into the db export folder ({@link BBConfiguration#getDBBackupDir()}) * * @return a 200 ok code and a json representation containing the list of files stored in the db backup folder */ @With({RootCredentialWrapFilter.class,ConnectToDBFilter.class}) public static Result getExports(){ List<String> fileNames = DbManagerService.getExports(); return ok(Json.toJson(fileNames)); } /** * /admin/db/import (POST) * * the method allows to upload a json export file and apply it to the db. * WARNING: all data on the db will be wiped out before importing * * @return a 200 Status code when the import is successfull,a 500 status code otherwise */ @With({RootCredentialWrapFilter.class,ConnectToDBFilter.class}) public static Result importDb(){ String appcode = (String)ctx().args.get("appcode"); MultipartFormData body = request().body().asMultipartFormData(); if (body==null) return badRequest("missing data: is the body multipart/form-data?"); FilePart fp = body.getFile("file"); if (fp!=null){ ZipInputStream zis = null; try{ java.io.File multipartFile=fp.getFile(); java.util.UUID uuid = java.util.UUID.randomUUID(); File zipFile = File.createTempFile(uuid.toString(), ".zip"); FileUtils.copyFile(multipartFile,zipFile); zis = new ZipInputStream(new FileInputStream(zipFile)); DbManagerService.importDb(appcode, zis); zipFile.delete(); return ok(); }catch(Exception e){ BaasBoxLogger.error(ExceptionUtils.getStackTrace(e)); return internalServerError(ExceptionUtils.getStackTrace(e)); }finally{ try { if(zis!=null){ zis.close(); } } catch (IOException e) { // Nothing to do here } } }else{ return badRequest("The form was submitted without a multipart file field."); } } //DB size and alert thresholds /** * /root/configuration (POST) * * this method allows to set (or override) just two configuration parameter (at the moment) * the db size Threshold in bytes: * baasbox.db.size * A percentage needed by the console to show alerts on dashboard when DB size is near the defined Threshold * baasbox.db.alert * * @return a 200 OK with the new values */ @With({RootCredentialWrapFilter.class}) public static Result overrideConfiguration(){ Http.RequestBody body = request().body(); JsonNode bodyJson= body.asJson(); JsonNode newDBAlert = bodyJson.get(BBConfiguration.DB_ALERT_THRESHOLD); JsonNode newDBSize = bodyJson.get(BBConfiguration.DB_SIZE_THRESHOLD); try{ if (newDBAlert!=null && !newDBAlert.isInt() && newDBAlert.asInt()<1) throw new IllegalArgumentException(BBConfiguration.DB_ALERT_THRESHOLD + " must be a positive integer value"); if (newDBSize!=null && !newDBSize.isLong() && newDBSize.asInt()<0) throw new IllegalArgumentException(BBConfiguration.DB_SIZE_THRESHOLD + " must be a positive integer value, or 0 to disable it"); }catch (Throwable e){ return badRequest(ExceptionUtils.getMessage(e)); } if (newDBAlert!=null) BBConfiguration.setDBAlertThreshold(newDBAlert.asInt()); if (newDBSize!=null) BBConfiguration.setDBSizeThreshold(BigInteger.valueOf(newDBSize.asLong())); HashMap returnMap = new HashMap(); returnMap.put(BBConfiguration.DB_ALERT_THRESHOLD, BBConfiguration.getDBAlertThreshold()); returnMap.put(BBConfiguration.DB_SIZE_THRESHOLD, BBConfiguration.getDBSizeThreshold()); try { return ok(new ObjectMapper().writeValueAsString(returnMap)); } catch (JsonProcessingException e) { return internalServerError(ExceptionUtils.getMessage(e)); } } @With({RootCredentialWrapFilter.class}) public static Result getOverridableConfiguration(){ HashMap returnMap = new HashMap(); returnMap.put(BBConfiguration.DB_ALERT_THRESHOLD, BBConfiguration.getDBAlertThreshold()); returnMap.put(BBConfiguration.DB_SIZE_THRESHOLD, BBConfiguration.getDBSizeThreshold()); try { return ok(new ObjectMapper().writeValueAsString(returnMap)); } catch (JsonProcessingException e) { return internalServerError(ExceptionUtils.getMessage(e)); } } }