This closes #832
Project: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/commit/f092e183 Tree: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/tree/f092e183 Diff: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/diff/f092e183 Branch: refs/heads/master Commit: f092e183f9fc0ebc060d615a0efdea78357d9f87 Parents: bf2cfcd 3fff641 Author: Alex Heneveld <[email protected]> Authored: Tue Aug 18 12:04:55 2015 +0100 Committer: Alex Heneveld <[email protected]> Committed: Tue Aug 18 12:04:55 2015 +0100 ---------------------------------------------------------------------- .../database/postgresql/PostgreSqlNode.java | 23 ++++++- .../postgresql/PostgreSqlSshDriver.java | 64 +++++++++++++++++--- 2 files changed, 77 insertions(+), 10 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f092e183/software/database/src/main/java/org/apache/brooklyn/entity/database/postgresql/PostgreSqlNode.java ---------------------------------------------------------------------- diff --cc software/database/src/main/java/org/apache/brooklyn/entity/database/postgresql/PostgreSqlNode.java index 7d195f7,0000000..70ac0c7 mode 100644,000000..100644 --- a/software/database/src/main/java/org/apache/brooklyn/entity/database/postgresql/PostgreSqlNode.java +++ b/software/database/src/main/java/org/apache/brooklyn/entity/database/postgresql/PostgreSqlNode.java @@@ -1,95 -1,0 +1,116 @@@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.brooklyn.entity.database.postgresql; + +import org.apache.brooklyn.api.catalog.Catalog; +import org.apache.brooklyn.api.entity.Effector; +import org.apache.brooklyn.api.entity.proxying.ImplementedBy; +import org.apache.brooklyn.api.entity.trait.HasShortName; +import org.apache.brooklyn.core.util.flags.SetFromFlag; + +import brooklyn.config.ConfigKey; +import brooklyn.entity.basic.ConfigKeys; +import brooklyn.entity.basic.SoftwareProcess; +import org.apache.brooklyn.entity.database.DatabaseNode; +import org.apache.brooklyn.entity.database.DatastoreMixins; +import org.apache.brooklyn.entity.database.DatastoreMixins.DatastoreCommon; +import brooklyn.entity.effector.Effectors; ++import brooklyn.event.basic.BasicAttributeSensorAndConfigKey; +import brooklyn.event.basic.PortAttributeSensorAndConfigKey; - +import org.apache.brooklyn.location.basic.PortRanges; + +/** + * PostgreSQL database node entity. + * <p> + * <ul> + * <li>You may need to increase shared memory settings in the kernel depending on the setting of + * the {@link #SHARED_MEMORY_BUFFER} key. The minimumm value is <em>128kB</em>. See the PostgreSQL + * <a href="http://www.postgresql.org/docs/9.1/static/kernel-resources.html">documentation</a>. + * <li>You will also need to enable passwordless sudo. + * </ul> + */ +@Catalog(name="PostgreSQL Node", description="PostgreSQL is an object-relational database management system (ORDBMS)", iconUrl="classpath:///postgresql-logo-200px.png") +@ImplementedBy(PostgreSqlNodeImpl.class) +public interface PostgreSqlNode extends SoftwareProcess, HasShortName, DatastoreCommon, DatabaseNode { + + @SetFromFlag("version") + ConfigKey<String> SUGGESTED_VERSION = ConfigKeys.newConfigKeyWithDefault(SoftwareProcess.SUGGESTED_VERSION, "9.3-1");//"9.1-4"); + + @SetFromFlag("configFileUrl") + ConfigKey<String> CONFIGURATION_FILE_URL = ConfigKeys.newStringConfigKey( + "postgresql.config.file.url", "URL where PostgreSQL configuration file can be found; " + + "if not supplied the blueprint uses the default and customises it"); + + @SetFromFlag("authConfigFileUrl") + ConfigKey<String> AUTHENTICATION_CONFIGURATION_FILE_URL = ConfigKeys.newStringConfigKey( + "postgresql.authConfig.file.url", "URL where PostgreSQL host-based authentication configuration file can be found; " + + "if not supplied the blueprint uses the default and customises it"); + + @SetFromFlag("port") + PortAttributeSensorAndConfigKey POSTGRESQL_PORT = new PortAttributeSensorAndConfigKey( + "postgresql.port", "PostgreSQL port", PortRanges.fromString("5432+")); + + @SetFromFlag("sharedMemory") + ConfigKey<String> SHARED_MEMORY = ConfigKeys.newStringConfigKey( + "postgresql.sharedMemory", "Size of shared memory buffer (must specify as kB, MB or GB, minimum 128kB)", "4MB"); + + @SetFromFlag("maxConnections") + ConfigKey<Integer> MAX_CONNECTIONS = ConfigKeys.newIntegerConfigKey( + "postgresql.maxConnections", "Maximum number of connections to the database", 100); + + @SetFromFlag("disconnectOnStop") + ConfigKey<Boolean> DISCONNECT_ON_STOP = ConfigKeys.newBooleanConfigKey( + "postgresql.disconnect.on.stop", "If true, PostgreSQL will immediately disconnet (pg_ctl -m immediate stop) all current connections when the node is stopped", true); + + @SetFromFlag("pollPeriod") + ConfigKey<Long> POLL_PERIOD = ConfigKeys.newLongConfigKey( + "postgresql.sensorpoll", "Poll period (in milliseconds)", 1000L); ++ ++ @SetFromFlag("initializeDB") ++ ConfigKey<Boolean> INITIALIZE_DB = ConfigKeys.newBooleanConfigKey( ++ "postgresql.initialize", "If true, PostgreSQL will create a new user and database", false); ++ ++ @SetFromFlag("username") ++ BasicAttributeSensorAndConfigKey<String> USERNAME = new BasicAttributeSensorAndConfigKey<>( ++ String.class, "postgresql.username", "Username of the database user"); ++ ++ String DEFAULT_USERNAME = "postgresqluser"; ++ ++ @SetFromFlag("password") ++ BasicAttributeSensorAndConfigKey<String> PASSWORD = new BasicAttributeSensorAndConfigKey<>( ++ String.class, "postgresql.password", ++ "Password for the database user, auto-generated if not set"); ++ ++ @SetFromFlag("database") ++ BasicAttributeSensorAndConfigKey<String> DATABASE = new BasicAttributeSensorAndConfigKey<>( ++ String.class, "postgresql.database", "Database to be used"); ++ ++ String DEFAULT_DB_NAME = "db"; + + Effector<String> EXECUTE_SCRIPT = Effectors.effector(DatastoreMixins.EXECUTE_SCRIPT) + .description("Executes the given script contents using psql") + .buildAbstract(); + + Integer getPostgreSqlPort(); + String getSharedMemory(); + Integer getMaxConnections(); + + String executeScript(String commands); + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f092e183/software/database/src/main/java/org/apache/brooklyn/entity/database/postgresql/PostgreSqlSshDriver.java ---------------------------------------------------------------------- diff --cc software/database/src/main/java/org/apache/brooklyn/entity/database/postgresql/PostgreSqlSshDriver.java index d66ed76,0000000..54b88a2 mode 100644,000000..100644 --- a/software/database/src/main/java/org/apache/brooklyn/entity/database/postgresql/PostgreSqlSshDriver.java +++ b/software/database/src/main/java/org/apache/brooklyn/entity/database/postgresql/PostgreSqlSshDriver.java @@@ -1,425 -1,0 +1,471 @@@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.brooklyn.entity.database.postgresql; + +import static brooklyn.util.ssh.BashCommands.INSTALL_WGET; +import static brooklyn.util.ssh.BashCommands.alternativesGroup; +import static brooklyn.util.ssh.BashCommands.chainGroup; +import static brooklyn.util.ssh.BashCommands.dontRequireTtyForSudo; +import static brooklyn.util.ssh.BashCommands.executeCommandThenAsUserTeeOutputToFile; +import static brooklyn.util.ssh.BashCommands.fail; +import static brooklyn.util.ssh.BashCommands.ifExecutableElse0; +import static brooklyn.util.ssh.BashCommands.ifExecutableElse1; +import static brooklyn.util.ssh.BashCommands.installPackage; +import static brooklyn.util.ssh.BashCommands.sudo; +import static brooklyn.util.ssh.BashCommands.sudoAsUser; +import static brooklyn.util.ssh.BashCommands.warn; +import static java.lang.String.format; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +import javax.annotation.Nullable; + ++import org.apache.brooklyn.api.location.OsDetails; ++import org.apache.brooklyn.core.util.task.DynamicTasks; ++import org.apache.brooklyn.core.util.task.ssh.SshTasks; ++import org.apache.brooklyn.core.util.task.ssh.SshTasks.OnFailingTask; ++import org.apache.brooklyn.core.util.task.system.ProcessTaskWrapper; ++import org.apache.brooklyn.location.basic.SshMachineLocation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import brooklyn.entity.basic.AbstractSoftwareProcessSshDriver; +import brooklyn.entity.basic.Attributes; +import brooklyn.entity.basic.SoftwareProcess; +import org.apache.brooklyn.entity.database.DatastoreMixins; +import brooklyn.entity.software.SshEffectorTasks; - - import org.apache.brooklyn.api.location.OsDetails; - import org.apache.brooklyn.core.util.task.DynamicTasks; - import org.apache.brooklyn.core.util.task.ssh.SshTasks; - import org.apache.brooklyn.core.util.task.ssh.SshTasks.OnFailingTask; - import org.apache.brooklyn.core.util.task.system.ProcessTaskWrapper; - import org.apache.brooklyn.location.basic.SshMachineLocation; - ++import brooklyn.event.basic.BasicAttributeSensorAndConfigKey; +import brooklyn.util.collections.MutableList; +import brooklyn.util.collections.MutableMap; +import brooklyn.util.exceptions.Exceptions; +import brooklyn.util.net.Urls; +import brooklyn.util.os.Os; +import brooklyn.util.stream.Streams; +import brooklyn.util.text.Identifiers; ++import brooklyn.util.text.StringEscapes; +import brooklyn.util.text.StringFunctions; +import brooklyn.util.text.Strings; + +import com.google.common.base.Charsets; +import com.google.common.base.Function; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import com.google.common.io.Files; + +/** + * The SSH implementation of the {@link PostgreSqlDriver}. + */ +public class PostgreSqlSshDriver extends AbstractSoftwareProcessSshDriver implements PostgreSqlDriver { + + public static final Logger log = LoggerFactory.getLogger(PostgreSqlSshDriver.class); + + public PostgreSqlSshDriver(PostgreSqlNodeImpl entity, SshMachineLocation machine) { + super(entity, machine); + + entity.setAttribute(Attributes.LOG_FILE_LOCATION, getLogFile()); + } + + /* + * TODO this is much messier than we would like because postgres runs as user postgres, + * meaning the dirs must be RW by that user, and accessible (thus all parent paths), + * which may rule out putting it in a location used by the default user. + * Two irritating things: + * * currently we sometimes make up a different onbox base dir; + * * currently we put files to /tmp for staging + * Could investigate if it really needs to run as user postgres; + * could also see whether default user can be added to group postgres, + * and the run dir (and all parents) made accessible to group postgres. + */ + @Override + public void install() { + String version = getEntity().getConfig(SoftwareProcess.SUGGESTED_VERSION); + String majorMinorVersion = version.substring(0, version.lastIndexOf("-")); + String shortVersion = majorMinorVersion.replace(".", ""); + + String altTarget = "/opt/brooklyn/postgres/"; + String altInstallDir = Urls.mergePaths(altTarget, "install/"+majorMinorVersion); + + Iterable<String> pgctlLocations = ImmutableList.of( + altInstallDir+"/bin", + "/usr/lib/postgresql/"+majorMinorVersion+"/bin/", + "/opt/local/lib/postgresql"+shortVersion+"/bin/", + "/usr/pgsql-"+majorMinorVersion+"/bin", + "/usr/local/bin/", + "/usr/bin/", + "/bin/"); + + DynamicTasks.queueIfPossible(SshTasks.dontRequireTtyForSudo(getMachine(), + // sudo is absolutely required here, in customize we set user to postgres + OnFailingTask.FAIL)).orSubmitAndBlock(); + DynamicTasks.waitForLast(); + + // Check whether we can find a usable pg_ctl, and if not install one + MutableList<String> findOrInstall = MutableList.<String>of() + .append("which pg_ctl") + .appendAll(Iterables.transform(pgctlLocations, StringFunctions.formatter("test -x %s/pg_ctl"))) + .append(installPackage(ImmutableMap.of( + "yum", "postgresql"+shortVersion+" postgresql"+shortVersion+"-server", + "apt", "postgresql-"+majorMinorVersion, + "port", "postgresql"+shortVersion+" postgresql"+shortVersion+"-server" + ), null)) + // due to impl of installPackage, it will not come to the line below I don't think + .append(warn(format("WARNING: failed to find or install postgresql %s binaries", majorMinorVersion))); + + // Link to correct binaries folder (different versions of pg_ctl and psql don't always play well together) + MutableList<String> linkFromHere = MutableList.<String>of() + .append(ifExecutableElse1("pg_ctl", chainGroup( + "PG_EXECUTABLE=`which pg_ctl`", + "PG_DIR=`dirname $PG_EXECUTABLE`", + "echo 'found pg_ctl in '$PG_DIR' on path so linking PG bin/ to that dir'", + "ln -s $PG_DIR bin"))) + .appendAll(Iterables.transform(pgctlLocations, givenDirIfFileExistsInItLinkToDir("pg_ctl", "bin"))) + .append(fail(format("WARNING: failed to find postgresql %s binaries for pg_ctl, may already have another version installed; aborting", majorMinorVersion), 9)); + + newScript(INSTALLING) + .body.append( + dontRequireTtyForSudo(), + ifExecutableElse0("yum", getYumRepository(version, majorMinorVersion, shortVersion)), + ifExecutableElse0("apt-get", getAptRepository()), + "rm -f bin", // if left over from previous incomplete/failed install (not sure why that keeps happening!) + alternativesGroup(findOrInstall), + alternativesGroup(linkFromHere)) + .failOnNonZeroResultCode() + .queue(); + + // check that the proposed install dir is one that user postgres can access + if (DynamicTasks.queue(SshEffectorTasks.ssh(sudoAsUser("postgres", "ls "+getInstallDir())).allowingNonZeroExitCode() + .summary("check postgres user can access install dir")).asTask().getUnchecked()!=0) { + log.info("Postgres install dir "+getInstallDir()+" for "+getEntity()+" is not accessible to user 'postgres'; " + "using "+altInstallDir+" instead"); + String newRunDir = Urls.mergePaths(altTarget, "apps", getEntity().getApplication().getId(), getEntity().getId()); + if (DynamicTasks.queue(SshEffectorTasks.ssh("ls "+altInstallDir+"/pg_ctl").allowingNonZeroExitCode() + .summary("check whether "+altInstallDir+" is set up")).asTask().getUnchecked()==0) { + // alt target already exists with binary; nothing to do for install + } else { + DynamicTasks.queue(SshEffectorTasks.ssh( + "mkdir -p "+altInstallDir, + "rm -rf '"+altInstallDir+"'", + "mv "+getInstallDir()+" "+altInstallDir, + "rm -rf '"+getInstallDir()+"'", + "ln -s "+altInstallDir+" "+getInstallDir(), + "mkdir -p " + newRunDir, + "chown -R postgres:postgres "+altTarget).runAsRoot().requiringExitCodeZero() + .summary("move install dir from user to postgres owned space")); + } + DynamicTasks.waitForLast(); + setInstallDir(altInstallDir); + setRunDir(newRunDir); + } + } + + private String getYumRepository(String version, String majorMinorVersion, String shortVersion) { + // postgres becomes available if you add the repos using an RPM such as + // http://yum.postgresql.org/9.3/redhat/rhel-6-i386/pgdg-centos93-9.3-1.noarch.rpm + // fedora, rhel, sl, and centos supported for RPM's + + OsDetails osDetails = getMachine().getMachineDetails().getOsDetails(); + String arch = osDetails.getArch(); + String osMajorVersion = osDetails.getVersion(); + String osName = osDetails.getName(); + + log.debug("postgres detecting yum information for "+getEntity()+" at "+getMachine()+": "+osName+", "+osMajorVersion+", "+arch); + + if (osName==null) osName = ""; else osName = osName.toLowerCase(); + + if (osName.equals("ubuntu")) return "echo skipping yum repo setup as this is not an rpm environment"; + + if (osName.equals("rhel")) osName = "redhat"; + else if (osName.equals("centos")) osName = "centos"; + else if (osName.equals("sl") || osName.startsWith("scientific")) osName = "sl"; + else if (osName.equals("fedora")) osName = "fedora"; + else { + log.debug("insufficient OS family information '"+osName+"' for "+getMachine()+" when installing "+getEntity()+" (yum repos); treating as centos"); + osName = "centos"; + } + + if (Strings.isBlank(arch)) { + log.warn("Insuffient architecture information '"+arch+"' for "+getMachine()+"when installing "+getEntity()+"; treating as x86_64"); + arch = "x86_64"; + } + + if (Strings.isBlank(osMajorVersion)) { + if (osName.equals("fedora")) osMajorVersion = "20"; + else osMajorVersion = "6"; + log.warn("Insuffient OS version information '"+getMachine().getOsDetails().getVersion()+"' for "+getMachine()+"when installing "+getEntity()+" (yum repos); treating as "+osMajorVersion); + } else { + if (osMajorVersion.indexOf(".")>0) + osMajorVersion = osMajorVersion.substring(0, osMajorVersion.indexOf('.')); + } + + return chainGroup( + INSTALL_WGET, + sudo(format("wget http://yum.postgresql.org/%s/redhat/rhel-%s-%s/pgdg-%s%s-%s.noarch.rpm", majorMinorVersion, osMajorVersion, arch, osName, shortVersion, version)), + sudo(format("rpm -Uvh pgdg-%s%s-%s.noarch.rpm", osName, shortVersion, version)) + ); + } + + private String getAptRepository() { + return chainGroup( + INSTALL_WGET, + "wget --quiet -O - http://apt.postgresql.org/pub/repos/apt/ACCC4CF8.asc | sudo tee -a apt-key add -", + "echo \"deb http://apt.postgresql.org/pub/repos/apt/ $(sudo lsb_release --codename --short)-pgdg main\" | sudo tee -a /etc/apt/sources.list.d/postgresql.list" + ); + } + + private static Function<String, String> givenDirIfFileExistsInItLinkToDir(final String filename, final String linkToMake) { + return new Function<String, String>() { + public String apply(@Nullable String dir) { + return ifExecutableElse1(Urls.mergePaths(dir, filename), + chainGroup("echo 'found "+filename+" in "+dir+" so linking to it in "+linkToMake+"'", "ln -s "+dir+" "+linkToMake)); + } + }; + } + + @Override + public void customize() { + // Some OSes start postgres during package installation + DynamicTasks.queue(SshEffectorTasks.ssh(sudoAsUser("postgres", "/etc/init.d/postgresql stop")).allowingNonZeroExitCode()).get(); + + newScript(CUSTOMIZING) + .body.append( + sudo("mkdir -p " + getDataDir()), + sudo("chown postgres:postgres " + getDataDir()), + sudo("chmod 700 " + getDataDir()), + sudo("touch " + getLogFile()), + sudo("chown postgres:postgres " + getLogFile()), + sudo("touch " + getPidFile()), + sudo("chown postgres:postgres " + getPidFile()), + alternativesGroup( + chainGroup(format("test -e %s", getInstallDir() + "/bin/initdb"), + sudoAsUser("postgres", getInstallDir() + "/bin/initdb -D " + getDataDir())), + callPgctl("initdb", true))) + .failOnNonZeroResultCode() + .execute(); + + String configUrl = getEntity().getConfig(PostgreSqlNode.CONFIGURATION_FILE_URL); + if (Strings.isBlank(configUrl)) { + // http://wiki.postgresql.org/wiki/Tuning_Your_PostgreSQL_Server + // If the same setting is listed multiple times, the last one wins. + DynamicTasks.queue(SshEffectorTasks.ssh( + executeCommandThenAsUserTeeOutputToFile( + chainGroup( + "echo \"listen_addresses = '*'\"", + "echo \"port = " + getEntity().getPostgreSqlPort() + "\"", + "echo \"max_connections = " + getEntity().getMaxConnections() + "\"", + "echo \"shared_buffers = " + getEntity().getSharedMemory() + "\"", + "echo \"external_pid_file = '" + getPidFile() + "'\""), + "postgres", getDataDir() + "/postgresql.conf"))); + } else { + String contents = processTemplate(configUrl); + DynamicTasks.queue( + SshEffectorTasks.put("/tmp/postgresql.conf").contents(contents), + SshEffectorTasks.ssh(sudoAsUser("postgres", "cp /tmp/postgresql.conf " + getDataDir() + "/postgresql.conf"))); + } + + String authConfigUrl = getEntity().getConfig(PostgreSqlNode.AUTHENTICATION_CONFIGURATION_FILE_URL); + if (Strings.isBlank(authConfigUrl)) { + DynamicTasks.queue(SshEffectorTasks.ssh( + // TODO give users control which hosts can connect and the authentication mechanism + executeCommandThenAsUserTeeOutputToFile("echo \"host all all 0.0.0.0/0 md5\"", "postgres", getDataDir() + "/pg_hba.conf"))); + } else { + String contents = processTemplate(authConfigUrl); + DynamicTasks.queue( + SshEffectorTasks.put("/tmp/pg_hba.conf").contents(contents), + SshEffectorTasks.ssh(sudoAsUser("postgres", "cp /tmp/pg_hba.conf " + getDataDir() + "/pg_hba.conf"))); + } + + // Wait for commands to complete before running the creation script + DynamicTasks.waitForLast(); - ++ if(entity.getConfig(PostgreSqlNode.INITIALIZE_DB)){ ++ initializeNewDatabase(); ++ } + // Capture log file contents if there is an error configuring the database + try { + executeDatabaseCreationScript(); + } catch (RuntimeException r) { + logTailOfPostgresLog(); + throw Exceptions.propagate(r); + } + + // Try establishing an external connection. If you get a "Connection refused...accepting TCP/IP connections + // on port 5432?" error then the port is probably closed. Check that the firewall allows external TCP/IP + // connections (netstat -nap). You can open a port with lokkit or by configuring the iptables. + } + ++ private void initializeNewDatabase() { ++ String createUserCommand = String.format( ++ "\"CREATE USER %s WITH PASSWORD '%s'; \"", ++ StringEscapes.escapeSql(getUsername()), ++ StringEscapes.escapeSql(getUserPassword()) ++ ); ++ String createDatabaseCommand = String.format( ++ "\"CREATE DATABASE %s OWNER %s\"", ++ StringEscapes.escapeSql(getDatabaseName()), ++ StringEscapes.escapeSql(getUsername())); ++ newScript("initializing user and database") ++ .body.append( ++ "cd " + getInstallDir(), ++ callPgctl("start", true), ++ sudoAsUser("postgres", getInstallDir() + "/bin/psql -p " + entity.getAttribute(PostgreSqlNode.POSTGRESQL_PORT) + ++ " --command="+ createUserCommand), ++ sudoAsUser("postgres", getInstallDir() + "/bin/psql -p " + entity.getAttribute(PostgreSqlNode.POSTGRESQL_PORT) + ++ " --command="+ createDatabaseCommand), ++ callPgctl("stop", true)) ++ .failOnNonZeroResultCode().execute(); ++ } ++ ++ private String getConfigOrDefault(BasicAttributeSensorAndConfigKey<String> key, String def) { ++ String config = entity.getConfig(key); ++ if(Strings.isEmpty(config)) { ++ config = def; ++ log.debug(entity + " has no config specified for " + key + "; using default `" + def + "`"); ++ entity.setAttribute(key, config); ++ } ++ return config; ++ } ++ ++ protected String getDatabaseName() { ++ return getConfigOrDefault(PostgreSqlNode.DATABASE, PostgreSqlNode.DEFAULT_DB_NAME); ++ } ++ ++ protected String getUsername(){ ++ return getConfigOrDefault(PostgreSqlNode.USERNAME, PostgreSqlNode.DEFAULT_USERNAME); ++ } ++ ++ protected String getUserPassword() { ++ return getConfigOrDefault(PostgreSqlNode.PASSWORD, Strings.makeRandomId(8)); ++ } ++ + protected void executeDatabaseCreationScript() { + if (copyDatabaseCreationScript()) { + newScript("running postgres creation script") + .body.append( + "cd " + getInstallDir(), + callPgctl("start", true), + sudoAsUser("postgres", getInstallDir() + "/bin/psql -p " + entity.getAttribute(PostgreSqlNode.POSTGRESQL_PORT) + " --file " + getRunDir() + "/creation-script.sql"), + callPgctl("stop", true)) + .failOnNonZeroResultCode() + .execute(); + } + } + + private boolean installFile(InputStream contents, String destName) { + String uid = Identifiers.makeRandomId(8); + // TODO currently put in /tmp for staging, since run dir may not be accessible to ssh user + getMachine().copyTo(contents, "/tmp/"+destName+"_"+uid); + DynamicTasks.queueIfPossible(SshEffectorTasks.ssh( + "cd "+getRunDir(), + "mv /tmp/"+destName+"_"+uid+" "+destName, + "chown postgres:postgres "+destName, + "chmod 644 "+destName) + .runAsRoot().requiringExitCodeZero()) + .orSubmitAndBlock(getEntity()).andWaitForSuccess(); + return true; + } + private boolean copyDatabaseCreationScript() { + InputStream creationScript = DatastoreMixins.getDatabaseCreationScript(entity); + if (creationScript==null) + return false; + return installFile(creationScript, "creation-script.sql"); + } + + public String getDataDir() { + return getRunDir() + "/data"; + } + + public String getLogFile() { + return getRunDir() + "/postgresql.log"; + } + + public String getPidFile() { + return getRunDir() + "/postgresql.pid"; + } + + /** @deprecated since 0.7.0 renamed {@link #logTailOfPostgresLog()} */ + @Deprecated + public void copyLogFileContents() { logTailOfPostgresLog(); } + public void logTailOfPostgresLog() { + try { + File file = Os.newTempFile("postgresql-"+getEntity().getId(), "log"); + int result = getMachine().copyFrom(getLogFile(), file.getAbsolutePath()); + if (result != 0) throw new IllegalStateException("Could not access log file " + getLogFile()); + log.info("Saving {} contents as {}", getLogFile(), file); + Streams.logStreamTail(log, "postgresql.log", Streams.byteArrayOfString(Files.toString(file, Charsets.UTF_8)), 1024); + file.delete(); + } catch (IOException ioe) { + log.debug("Error reading copied log file: {}", ioe); + } + } + + protected String callPgctl(String command, boolean waitForIt) { + return sudoAsUser("postgres", getInstallDir() + "/bin/pg_ctl -D " + getDataDir() + + " -l " + getLogFile() + (waitForIt ? " -w " : " ") + command); + } + + @Override + public void launch() { + log.info(String.format("Starting entity %s at %s", this, getLocation())); + newScript(MutableMap.of("usePidFile", false), LAUNCHING) + .body.append(callPgctl("start", false)) + .execute(); + } + + @Override + public boolean isRunning() { + return newScript(MutableMap.of("usePidFile", getPidFile()), CHECK_RUNNING) + .body.append(getStatusCmd()) + .execute() == 0; + } + + @Override + public void stop() { + newScript(MutableMap.of("usePidFile", false), STOPPING) + .body.append(callPgctl((entity.getConfig(PostgreSqlNode.DISCONNECT_ON_STOP) ? "-m immediate " : "") + "stop", false)) + .failOnNonZeroResultCode() + .execute(); + newScript(MutableMap.of("usePidFile", getPidFile(), "processOwner", "postgres"), STOPPING).execute(); + } + + @Override + public PostgreSqlNodeImpl getEntity() { + return (PostgreSqlNodeImpl) super.getEntity(); + } + + @Override + public String getStatusCmd() { + return callPgctl("status", false); + } + + public ProcessTaskWrapper<Integer> executeScriptAsync(String commands) { + String filename = "postgresql-commands-"+Identifiers.makeRandomId(8); + installFile(Streams.newInputStreamWithContents(commands), filename); + return executeScriptFromInstalledFileAsync(filename); + } + + public ProcessTaskWrapper<Integer> executeScriptFromInstalledFileAsync(String filenameAlreadyInstalledAtServer) { + return DynamicTasks.queue( + SshEffectorTasks.ssh( + "cd "+getRunDir(), + sudoAsUser("postgres", getInstallDir() + "/bin/psql -p " + entity.getAttribute(PostgreSqlNode.POSTGRESQL_PORT) + " --file " + filenameAlreadyInstalledAtServer)) + .summary("executing datastore script "+filenameAlreadyInstalledAtServer)); + } + +}
