package io.cattle.platform.iaas.api.manager; import io.cattle.platform.archaius.util.ArchaiusUtil; import io.cattle.platform.core.util.SettingsUtils; import io.cattle.platform.framework.encryption.impl.Aes256Encrypter; import io.cattle.platform.object.util.DataAccessor; import io.cattle.platform.util.type.CollectionUtils; import io.github.ibuildthecloud.gdapi.context.ApiContext; import io.github.ibuildthecloud.gdapi.exception.ClientVisibleException; import io.github.ibuildthecloud.gdapi.factory.SchemaFactory; import io.github.ibuildthecloud.gdapi.model.ListOptions; import io.github.ibuildthecloud.gdapi.model.Resource; import io.github.ibuildthecloud.gdapi.model.impl.ResourceImpl; import io.github.ibuildthecloud.gdapi.request.ApiRequest; import io.github.ibuildthecloud.gdapi.request.resource.impl.AbstractNoOpResourceManager; import io.github.ibuildthecloud.gdapi.url.UrlBuilder; import io.github.ibuildthecloud.gdapi.util.ResponseCodes; import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; import java.lang.ProcessBuilder.Redirect; import java.net.URL; import java.security.Key; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.TimeZone; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import javax.annotation.PostConstruct; import javax.inject.Inject; import javax.servlet.http.HttpServletResponse; import org.apache.commons.codec.binary.Base64; import org.apache.commons.io.IOUtils; import com.netflix.config.DynamicBooleanProperty; import com.netflix.config.DynamicIntProperty; import com.netflix.config.DynamicStringProperty; import freemarker.template.Configuration; import freemarker.template.Template; import freemarker.template.TemplateException; public class HaConfigManager extends AbstractNoOpResourceManager { public static DynamicStringProperty DB = ArchaiusUtil.getString("db.cattle.database"); public static DynamicStringProperty DB_HOST = DB.get().equals("mysql") ? ArchaiusUtil.getString("db.cattle.mysql.host") : ArchaiusUtil.getString("db.cattle.postgres.host"); public static DynamicStringProperty DB_PORT = DB.get().equals("mysql") ? ArchaiusUtil.getString("db.cattle.mysql.port") : ArchaiusUtil.getString("db.cattle.postgres.port"); public static DynamicStringProperty DB_NAME = DB.get().equals("mysql") ? ArchaiusUtil.getString("db.cattle.mysql.name") : ArchaiusUtil.getString("db.cattle.postgres.name"); public static DynamicStringProperty DB_USER = ArchaiusUtil.getString("db.cattle.username"); public static DynamicStringProperty DB_PASS = ArchaiusUtil.getString("db.cattle.password"); private static DynamicBooleanProperty HA_ENABLED = ArchaiusUtil.getBoolean("ha.enabled"); private static DynamicIntProperty HA_CLUSTER_SIZE = ArchaiusUtil.getInt("ha.cluster.size"); @Inject Aes256Encrypter encrypter; @Inject SettingsUtils settingsUtils; Configuration configuration; Template template; @Override protected Object listInternal(SchemaFactory schemaFactory, String type, Map<Object, Object> criteria, ListOptions options) { return Arrays.asList(getHaConfig()); } protected Resource getHaConfig() { Map<String, Object> data = new HashMap<>(); String host = DB_HOST.get(); data.put("dbHost", host); data.put("enabled", HA_ENABLED.get()); data.put("clusterSize", HA_CLUSTER_SIZE.get()); if ("localhost".equals(host) || "127.0.0.1".equals(host)) { try { data.put("dbSize", dbSize()); } catch (NumberFormatException | IOException e) { data.put("dbSize", -1); } } ResourceImpl resource = new ResourceImpl("haConfig", "haConfig", data); UrlBuilder builder = ApiContext.getUrlBuilder(); resource.getLinks().put("dbdump", builder.resourceLink(resource, "dbdump")); resource.getActions().put("createscript", builder.actionLink(resource, "createscript")); return resource; } @Override protected Object getLinkInternal(String type, String id, String link, ApiRequest request) { if ("dbdump".equalsIgnoreCase(link)) { try { return dbDump(request); } catch (IOException | InterruptedException e) { throw new ClientVisibleException(ResponseCodes.INTERNAL_SERVER_ERROR, "DumpFailed", "Failed to create database dump", e.getMessage()); } } return null; } @Override protected Object resourceActionInternal(Object obj, ApiRequest request) { if ("createscript".equalsIgnoreCase(request.getAction())) { try { return getScript(request); } catch (IOException | TemplateException e) { throw new ClientVisibleException(ResponseCodes.INTERNAL_SERVER_ERROR, "ScriptFailed", "Failed to create script", e.getMessage()); } } return null; } @Override protected Object updateInternal(String type, String id, Object obj, ApiRequest request) { Map<Object, Object> data = new HashMap<>(CollectionUtils.toMap(request.getRequestObject())); Boolean enabled = DataAccessor.fromMap(data).withKey("enabled").as(Boolean.class); if (enabled != null) { settingsUtils.changeSetting("ha.enabled", enabled.toString()); } return getHaConfig(); } protected Object getScript(ApiRequest request) throws IOException, TemplateException { Map<Object, Object> data = new HashMap<>(CollectionUtils.toMap(request.getRequestObject())); Key key = encrypter.generateKey(); Long clusterSize = DataAccessor.fromMap(data).withKey("clusterSize") .withDefault(3L).as(Long.class); settingsUtils.changeSetting("ha.cluster.size", clusterSize.toString()); data.put("encryptionKey", Base64.encodeBase64String(key.getEncoded())); data.put("db", DB.get()); data.put("dbHost", DB_HOST.get()); data.put("dbPort", DB_PORT.get()); data.put("dbUser", DB_USER.get()); data.put("dbPass", encrypter.encrypt(DB_PASS.get(), key)); data.put("dbName", DB_NAME.get()); data.put("containerPrefix", "rancher-ha-"); HttpServletResponse response = request.getServletContext().getResponse(); response.setContentType("application/octet-stream"); response.setHeader("Content-Disposition", "attachment; filename=rancher-ha.sh"); response.setHeader("Cache-Control", "private"); response.setHeader("Pragma", "private"); response.setHeader("Expires", "Sun 26 Jul 1981 18:42:00 GMT"); URL url = Thread.currentThread().getContextClassLoader().getResource("haconfig/script.ftl"); Template template = configuration.getTemplate(url.toExternalForm()); try (OutputStreamWriter writer = new OutputStreamWriter(request.getOutputStream())) { template.process(data, writer); } return new Object(); } protected long dbSize() throws IOException { ProcessBuilder pb = DB.get().equals("mysql") ? new ProcessBuilder("mysql", "--skip-column-names", "-s", "-uroot", "-e", "SELECT SUM(data_length)/power(1024,2) AS dbsize_mb FROM information_schema.tables WHERE table_schema='cattle' GROUP BY table_schema;") : new ProcessBuilder("psql", "cattle", "cattle", "-t", "-q", "-c", "SELECT pg_database_size('cattle')/power(1024,2)"); pb.redirectError(Redirect.INHERIT); Process p = pb.start(); try (InputStream in = p.getInputStream()) { return Long.parseLong(IOUtils.toString(in).split("[.]")[0].trim()); } } protected Object dbDump(ApiRequest request) throws IOException, InterruptedException { ProcessBuilder pb = DB.get().equals("mysql") ? new ProcessBuilder("mysqldump", "-uroot", "cattle") : new ProcessBuilder("pg_dump", "-Fc", "-Ucattle", "cattle"); pb.redirectError(Redirect.INHERIT); TimeZone tz = TimeZone.getTimeZone("UTC"); DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); df.setTimeZone(tz); String now = df.format(new Date()); HttpServletResponse response = request.getServletContext().getResponse(); String prefix = "rancher-db-dump-" + now; response.setContentType("application/octet-stream"); response.setHeader("Content-Disposition", "attachment; filename=" + prefix + ".zip"); response.setHeader("Cache-Control", "private"); response.setHeader("Pragma", "private"); response.setHeader("Expires", "Sun 26 Jul 1981 18:42:00 GMT"); Process p = pb.start(); try (InputStream in = p.getInputStream(); ZipOutputStream out = new ZipOutputStream(response.getOutputStream())) { out.putNextEntry(new ZipEntry(prefix + ".sql")); IOUtils.copyLarge(in, out); out.closeEntry(); } p.waitFor(); return new Object(); } @PostConstruct public void init() throws IOException { URL url = Thread.currentThread().getContextClassLoader().getResource("haconfig/script.ftl"); template = configuration.getTemplate(url.toExternalForm()); } @Override public String[] getTypes() { return new String[] {"haConfig"}; } @Override public Class<?>[] getTypeClasses() { return new Class<?>[0]; } public Configuration getConfiguration() { return configuration; } public void setConfiguration(Configuration configuration) { this.configuration = configuration; } }