Java tutorial
/* * Copyright (C) 2012-2015 DataStax Inc. * * 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.datastax.driver.core; import com.google.common.base.Joiner; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; import com.google.common.io.ByteStreams; import com.google.common.io.Closer; import com.google.common.io.Files; import org.apache.commons.exec.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.*; import java.net.InetSocketAddress; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; import static com.datastax.driver.core.TestUtils.*; public class CCMBridge implements CCMAccess { private static final Logger logger = LoggerFactory.getLogger(CCMBridge.class); private static final String CASSANDRA_VERSION; private static final Set<String> CASSANDRA_INSTALL_ARGS; private static final boolean IS_DSE; public static final String DEFAULT_CLIENT_TRUSTSTORE_PASSWORD = "cassandra1sfun"; public static final String DEFAULT_CLIENT_TRUSTSTORE_PATH = "/client.truststore"; public static final File DEFAULT_CLIENT_TRUSTSTORE_FILE = createTempStore(DEFAULT_CLIENT_TRUSTSTORE_PATH); public static final String DEFAULT_CLIENT_KEYSTORE_PASSWORD = "cassandra1sfun"; public static final String DEFAULT_CLIENT_KEYSTORE_PATH = "/client.keystore"; public static final File DEFAULT_CLIENT_KEYSTORE_FILE = createTempStore(DEFAULT_CLIENT_KEYSTORE_PATH); // Contain the same keypair as the client keystore, but in format usable by OpenSSL public static final File DEFAULT_CLIENT_PRIVATE_KEY_FILE = createTempStore("/client.key"); public static final File DEFAULT_CLIENT_CERT_CHAIN_FILE = createTempStore("/client.crt"); public static final String DEFAULT_SERVER_TRUSTSTORE_PASSWORD = "cassandra1sfun"; public static final String DEFAULT_SERVER_TRUSTSTORE_PATH = "/server.truststore"; private static final File DEFAULT_SERVER_TRUSTSTORE_FILE = createTempStore(DEFAULT_SERVER_TRUSTSTORE_PATH); public static final String DEFAULT_SERVER_KEYSTORE_PASSWORD = "cassandra1sfun"; public static final String DEFAULT_SERVER_KEYSTORE_PATH = "/server.keystore"; private static final File DEFAULT_SERVER_KEYSTORE_FILE = createTempStore(DEFAULT_SERVER_KEYSTORE_PATH); /** * The environment variables to use when invoking CCM. Inherits the current processes environment, but will also * prepend to the PATH variable the value of the 'ccm.path' property and set JAVA_HOME variable to the * 'ccm.java.home' variable. * <p/> * At times it is necessary to use a separate java install for CCM then what is being used for running tests. * For example, if you want to run tests with JDK 6 but against Cassandra 2.0, which requires JDK 7. */ private static final Map<String, String> ENVIRONMENT_MAP; /** * A mapping of full DSE versions to their C* counterpart. This is not meant to be comprehensive. Used by * {@link #getCassandraVersion()}. If C* version cannot be derived, the method makes a 'best guess'. */ private static final Map<String, String> dseToCassandraVersions = ImmutableMap.<String, String>builder() .put("5.0.4", "3.0.10").put("5.0.3", "3.0.9").put("5.0.2", "3.0.8").put("5.0.1", "3.0.7") .put("5.0", "3.0.7").put("4.8.11", "2.1.17").put("4.8.10", "2.1.15").put("4.8.9", "2.1.15") .put("4.8.8", "2.1.14").put("4.8.7", "2.1.14").put("4.8.6", "2.1.13").put("4.8.5", "2.1.13") .put("4.8.4", "2.1.12").put("4.8.3", "2.1.11").put("4.8.2", "2.1.11").put("4.8.1", "2.1.11") .put("4.8", "2.1.9").put("4.7.9", "2.1.15").put("4.7.8", "2.1.13").put("4.7.7", "2.1.12") .put("4.7.6", "2.1.11").put("4.7.5", "2.1.11").put("4.7.4", "2.1.11").put("4.7.3", "2.1.8") .put("4.7.2", "2.1.8").put("4.7.1", "2.1.5").put("4.6.11", "2.0.16").put("4.6.10", "2.0.16") .put("4.6.9", "2.0.16").put("4.6.8", "2.0.16").put("4.6.7", "2.0.14").put("4.6.6", "2.0.14") .put("4.6.5", "2.0.14").put("4.6.4", "2.0.14").put("4.6.3", "2.0.12").put("4.6.2", "2.0.12") .put("4.6.1", "2.0.12").put("4.6", "2.0.11").put("4.5.9", "2.0.16").put("4.5.8", "2.0.14") .put("4.5.7", "2.0.12").put("4.5.6", "2.0.12").put("4.5.5", "2.0.12").put("4.5.4", "2.0.11") .put("4.5.3", "2.0.11").put("4.5.2", "2.0.10").put("4.5.1", "2.0.8").put("4.5", "2.0.8") .put("4.0", "2.0").put("3.2", "1.2").put("3.1", "1.2").build(); /** * The command to use to launch CCM */ private static final String CCM_COMMAND; static { CASSANDRA_VERSION = System.getProperty("cassandra.version"); String installDirectory = System.getProperty("cassandra.directory"); String branch = System.getProperty("cassandra.branch"); String dseProperty = System.getProperty("dse"); // If -Ddse, if the value is empty interpret it as enabled, // otherwise if there is a value, parse as boolean. IS_DSE = dseProperty != null && (dseProperty.isEmpty() || Boolean.parseBoolean(dseProperty)); ImmutableSet.Builder<String> installArgs = ImmutableSet.builder(); if (installDirectory != null && !installDirectory.trim().isEmpty()) { installArgs.add("--install-dir=" + new File(installDirectory).getAbsolutePath()); } else if (branch != null && !branch.trim().isEmpty()) { installArgs.add("-v git:" + branch.trim().replaceAll("\"", "")); } else { installArgs.add("-v " + CASSANDRA_VERSION); } if (IS_DSE) { installArgs.add("--dse"); } CASSANDRA_INSTALL_ARGS = installArgs.build(); // Inherit the current environment. Map<String, String> envMap = Maps.newHashMap(new ProcessBuilder().environment()); // If ccm.path is set, override the PATH variable with it. String ccmPath = System.getProperty("ccm.path"); if (ccmPath != null) { String existingPath = envMap.get("PATH"); if (existingPath == null) { existingPath = ""; } envMap.put("PATH", ccmPath + File.pathSeparator + existingPath); } if (isWindows()) { CCM_COMMAND = "powershell.exe -ExecutionPolicy Unrestricted ccm.py"; } else { CCM_COMMAND = "ccm"; } // If ccm.java.home is set, override the JAVA_HOME variable with it. String ccmJavaHome = System.getProperty("ccm.java.home"); if (ccmJavaHome != null) { envMap.put("JAVA_HOME", ccmJavaHome); } ENVIRONMENT_MAP = ImmutableMap.copyOf(envMap); if (CCMBridge.isDSE()) { logger.info("Tests requiring CCM will by default use DSE version {} (C* {}, install arguments: {})", CCMBridge.getDSEVersion(), CCMBridge.getCassandraVersion(), CCMBridge.getInstallArguments()); } else { logger.info("Tests requiring CCM will by default use Cassandra version {} (install arguments: {})", CCMBridge.getCassandraVersion(), CCMBridge.getInstallArguments()); } } /** * Checks if the operating system is a Windows one * * @return <code>true</code> if the operating system is a Windows one, <code>false</code> otherwise. */ public static boolean isWindows() { String osName = System.getProperty("os.name"); return osName != null && osName.startsWith("Windows"); } private final String clusterName; private final VersionNumber version; private final int storagePort; private final int thriftPort; private final int binaryPort; private final File ccmDir; private final boolean isDSE; private final String jvmArgs; private boolean keepLogs = false; private boolean started = false; private boolean closed = false; private final int[] nodes; private CCMBridge(String clusterName, boolean isDSE, VersionNumber version, int storagePort, int thriftPort, int binaryPort, String jvmArgs, int[] nodes) { this.clusterName = clusterName; this.version = version; this.storagePort = storagePort; this.thriftPort = thriftPort; this.binaryPort = binaryPort; this.isDSE = isDSE; this.jvmArgs = jvmArgs; this.nodes = nodes; this.ccmDir = Files.createTempDir(); } /** * @return The configured cassandra version. If -Ddse=true was used, this value is derived from the * DSE version provided. If the DSE version can't be derived the following logic is used: * <ol> * <li>If <= 3.X, use C* 1.2</li> * <li>If 4.X, use 2.1 for >= 4.7, 2.0 otherwise.</li> * <li>Otherwise 3.0</li> * </ol> */ public static String getCassandraVersion() { if (isDSE()) { String cassandraVersion = dseToCassandraVersions.get(CASSANDRA_VERSION); if (cassandraVersion != null) { return cassandraVersion; } else if (CASSANDRA_VERSION.startsWith("3.") || CASSANDRA_VERSION.compareTo("3") <= 0) { return "1.2"; } else if (CASSANDRA_VERSION.startsWith("4.")) { if (CASSANDRA_VERSION.compareTo("4.7") >= 0) { return "2.1"; } else { return "2.0"; } } else { // Fallback on 3.0 by default. return "3.0"; } } else { return CASSANDRA_VERSION; } } /** * @return The configured DSE version if '-Ddse=true' specified, otherwise null. */ public static String getDSEVersion() { if (isDSE()) { return CASSANDRA_VERSION; } else { return null; } } /** * @return Whether or not DSE was configured via '-Ddse=true'. */ public static boolean isDSE() { return IS_DSE; } /** * @return The install arguments to pass to CCM when creating the cluster. */ public static Set<String> getInstallArguments() { return CASSANDRA_INSTALL_ARGS; } public static Builder builder() { return new Builder(); } @Override public String getClusterName() { return clusterName; } @Override public InetSocketAddress addressOfNode(int n) { return new InetSocketAddress(TestUtils.ipOfNode(n), binaryPort); } @Override public VersionNumber getVersion() { return version; } @Override public File getCcmDir() { return ccmDir; } @Override public File getClusterDir() { return new File(ccmDir, clusterName); } @Override public File getNodeDir(int n) { return new File(getClusterDir(), "node" + n); } @Override public File getNodeConfDir(int n) { return new File(getNodeDir(n), "conf"); } @Override public int getStoragePort() { return storagePort; } @Override public int getThriftPort() { return thriftPort; } @Override public int getBinaryPort() { return binaryPort; } @Override public void setKeepLogs(boolean keepLogs) { this.keepLogs = keepLogs; } @Override public synchronized void close() { if (closed) return; logger.debug("Closing: {}", this); if (keepLogs) { executeNoFail(new Runnable() { @Override public void run() { stop(); } }, false); logger.info("Error during tests, kept C* logs in " + getCcmDir()); } else { executeNoFail(new Runnable() { @Override public void run() { remove(); } }, false); executeNoFail(new Runnable() { @Override public void run() { org.assertj.core.util.Files.delete(getCcmDir()); } }, false); } closed = true; logger.debug("Closed: {}", this); } /** * Based on C* version, return the wait arguments. * * @return For C* 1.x, --wait-other-notice otherwise --no-wait */ private String getStartWaitArguments() { // make a small exception for C* 1.2 as it has a bug where it starts listening on the binary // interface slightly before it joins the cluster. if (getCassandraVersion().startsWith("1.")) { return " --wait-other-notice"; } else { return " --no-wait"; } } @Override public synchronized void start() { if (started) return; if (logger.isDebugEnabled()) logger.debug("Starting: {} - free memory: {} MB", this, TestUtils.getFreeMemoryMB()); try { String cmd = CCM_COMMAND + " start " + jvmArgs + getStartWaitArguments(); if (isWindows() && this.version.compareTo(VersionNumber.parse("2.2.4")) >= 0) { cmd += " --quiet-windows"; } execute(cmd); // Wait for binary interface on each node. int n = 1; for (int dc = 1; dc <= nodes.length; dc++) { int nodesInDc = nodes[dc - 1]; for (int i = 0; i < nodesInDc; i++) { InetSocketAddress addr = new InetSocketAddress(ipOfNode(n), binaryPort); logger.debug("Waiting for binary protocol to show up for {}", addr); TestUtils.waitUntilPortIsUp(addr); n++; } } } catch (CCMException e) { logger.error("Could not start " + this, e); logger.error("CCM output:\n{}", e.getOut()); setKeepLogs(true); String errors = checkForErrors(); if (errors != null) logger.error("CCM check errors:\n{}", errors); throw e; } if (logger.isDebugEnabled()) logger.debug("Started: {} - Free memory: {} MB", this, TestUtils.getFreeMemoryMB()); started = true; } @Override public synchronized void stop() { if (closed) return; if (logger.isDebugEnabled()) logger.debug("Stopping: {} - free memory: {} MB", this, TestUtils.getFreeMemoryMB()); execute(CCM_COMMAND + " stop"); if (logger.isDebugEnabled()) logger.debug("Stopped: {} - free memory: {} MB", this, TestUtils.getFreeMemoryMB()); closed = true; } @Override public synchronized void forceStop() { if (closed) return; logger.debug("Force stopping: {}", this); execute(CCM_COMMAND + " stop --not-gently"); closed = true; } @Override public synchronized void remove() { stop(); logger.debug("Removing: {}", this); execute(CCM_COMMAND + " remove"); } @Override public String checkForErrors() { logger.debug("Checking for errors in: {}", this); try { return execute(CCM_COMMAND + " checklogerror"); } catch (CCMException e) { logger.warn("Check for errors failed"); return null; } } @Override public void start(int n) { logger.debug( String.format("Starting: node %s (%s%s:%s) in %s", n, TestUtils.IP_PREFIX, n, binaryPort, this)); try { String cmd = CCM_COMMAND + " node%d start " + jvmArgs + getStartWaitArguments(); if (isWindows() && this.version.compareTo(VersionNumber.parse("2.2.4")) >= 0) { cmd += " --quiet-windows"; } execute(cmd, n); // Wait for binary interface InetSocketAddress addr = new InetSocketAddress(ipOfNode(n), binaryPort); logger.debug("Waiting for binary protocol to show up for {}", addr); TestUtils.waitUntilPortIsUp(addr); } catch (CCMException e) { logger.error(String.format("Could not start node %s in %s", n, this), e); logger.error("CCM output:\n{}", e.getOut()); setKeepLogs(true); String errors = checkForErrors(); if (errors != null) logger.error("CCM check errors:\n{}", errors); throw e; } } @Override public void stop(int n) { logger.debug( String.format("Stopping: node %s (%s%s:%s) in %s", n, TestUtils.IP_PREFIX, n, binaryPort, this)); execute(CCM_COMMAND + " node%d stop", n); } @Override public void forceStop(int n) { logger.debug(String.format("Force stopping: node %s (%s%s:%s) in %s", n, TestUtils.IP_PREFIX, n, binaryPort, this)); execute(CCM_COMMAND + " node%d stop --not-gently", n); } @Override public void remove(int n) { logger.debug( String.format("Removing: node %s (%s%s:%s) from %s", n, TestUtils.IP_PREFIX, n, binaryPort, this)); execute(CCM_COMMAND + " node%d remove", n); } @Override public void add(int n) { add(1, n); } @Override public void add(int dc, int n) { logger.debug(String.format("Adding: node %s (%s%s:%s) to %s", n, TestUtils.IP_PREFIX, n, binaryPort, this)); String thriftItf = TestUtils.ipOfNode(n) + ":" + thriftPort; String storageItf = TestUtils.ipOfNode(n) + ":" + storagePort; String binaryItf = TestUtils.ipOfNode(n) + ":" + binaryPort; String remoteLogItf = TestUtils.ipOfNode(n) + ":" + TestUtils.findAvailablePort(); execute(CCM_COMMAND + " add node%d -d dc%s -i %s%d -t %s -l %s --binary-itf %s -j %d -r %s -s -b" + (isDSE ? " --dse" : ""), n, dc, TestUtils.IP_PREFIX, n, thriftItf, storageItf, binaryItf, TestUtils.findAvailablePort(), remoteLogItf); } @Override public void decommission(int n) { logger.debug(String.format("Decommissioning: node %s (%s%s:%s) from %s", n, TestUtils.IP_PREFIX, n, binaryPort, this)); execute(CCM_COMMAND + " node%d decommission", n); } @Override public void updateConfig(Map<String, Object> configs) { StringBuilder confStr = new StringBuilder(); for (Map.Entry<String, Object> entry : configs.entrySet()) { confStr.append(entry.getKey()).append(":").append(entry.getValue()).append(" "); } execute(CCM_COMMAND + " updateconf " + confStr); } @Override public void updateDSEConfig(Map<String, Object> configs) { StringBuilder confStr = new StringBuilder(); for (Map.Entry<String, Object> entry : configs.entrySet()) { confStr.append(entry.getKey()).append(":").append(entry.getValue()).append(" "); } execute(CCM_COMMAND + " updatedseconf " + confStr); } @Override public void updateNodeConfig(int n, String key, Object value) { updateNodeConfig(n, ImmutableMap.<String, Object>builder().put(key, value).build()); } @Override public void updateNodeConfig(int n, Map<String, Object> configs) { StringBuilder confStr = new StringBuilder(); for (Map.Entry<String, Object> entry : configs.entrySet()) { confStr.append(entry.getKey()).append(":").append(entry.getValue()).append(" "); } execute(CCM_COMMAND + " node%s updateconf %s", n, confStr); } @Override public void updateDSENodeConfig(int n, String key, Object value) { updateDSENodeConfig(n, ImmutableMap.<String, Object>builder().put(key, value).build()); } @Override public void updateDSENodeConfig(int n, Map<String, Object> configs) { StringBuilder confStr = new StringBuilder(); for (Map.Entry<String, Object> entry : configs.entrySet()) { confStr.append(entry.getKey()).append(":").append(entry.getValue()).append(" "); } execute(CCM_COMMAND + " node%s updatedseconf %s", n, confStr); } @Override public void setWorkload(int node, Workload... workload) { String workloadStr = Joiner.on(",").join(workload); execute(CCM_COMMAND + " node%d setworkload %s", node, workloadStr); } private String execute(String command, Object... args) { String fullCommand = String.format(command, args) + " --config-dir=" + ccmDir; Closer closer = Closer.create(); // 10 minutes timeout ExecuteWatchdog watchDog = new ExecuteWatchdog(TimeUnit.MINUTES.toMillis(10)); StringWriter sw = new StringWriter(); final PrintWriter pw = new PrintWriter(sw); closer.register(pw); try { logger.trace("Executing: " + fullCommand); CommandLine cli = CommandLine.parse(fullCommand); Executor executor = new DefaultExecutor(); LogOutputStream outStream = new LogOutputStream() { @Override protected void processLine(String line, int logLevel) { String out = "ccmout> " + line; logger.debug(out); pw.println(out); } }; LogOutputStream errStream = new LogOutputStream() { @Override protected void processLine(String line, int logLevel) { String err = "ccmerr> " + line; logger.error(err); pw.println(err); } }; closer.register(outStream); closer.register(errStream); ExecuteStreamHandler streamHandler = new PumpStreamHandler(outStream, errStream); executor.setStreamHandler(streamHandler); executor.setWatchdog(watchDog); int retValue = executor.execute(cli, ENVIRONMENT_MAP); if (retValue != 0) { logger.error("Non-zero exit code ({}) returned from executing ccm command: {}", retValue, fullCommand); pw.flush(); throw new CCMException( String.format("Non-zero exit code (%s) returned from executing ccm command: %s", retValue, fullCommand), sw.toString()); } } catch (IOException e) { if (watchDog.killedProcess()) logger.error("The command {} was killed after 10 minutes", fullCommand); pw.flush(); throw new CCMException(String.format("The command %s failed to execute", fullCommand), sw.toString(), e); } finally { try { closer.close(); } catch (IOException e) { Throwables.propagate(e); } } return sw.toString(); } /** * Waits for a host to be up by pinging the TCP socket directly, without using the Java driver's API. */ public void waitForUp(int node) { TestUtils.waitUntilPortIsUp(addressOfNode(node)); } /** * Waits for a host to be down by pinging the TCP socket directly, without using the Java driver's API. */ public void waitForDown(int node) { TestUtils.waitUntilPortIsDown(addressOfNode(node)); } /** * <p> * Extracts a keystore from the classpath into a temporary file. * </p> * <p/> * <p> * This is needed as the keystore could be part of a built test jar used by other * projects, and they need to be extracted to a file system so cassandra may use them. * </p> * * @param storePath Path in classpath where the keystore exists. * @return The generated File. */ private static File createTempStore(String storePath) { File f = null; Closer closer = Closer.create(); try { InputStream trustStoreIs = CCMBridge.class.getResourceAsStream(storePath); closer.register(trustStoreIs); f = File.createTempFile("server", ".store"); logger.debug("Created store file {} for {}.", f, storePath); OutputStream trustStoreOs = new FileOutputStream(f); closer.register(trustStoreOs); ByteStreams.copy(trustStoreIs, trustStoreOs); } catch (IOException e) { logger.warn("Failure to write keystore, SSL-enabled servers may fail to start.", e); } finally { try { closer.close(); } catch (IOException e) { logger.warn("Failure closing streams.", e); } } return f; } @Override public String toString() { return "CCM cluster " + clusterName; } @Override protected void finalize() throws Throwable { logger.debug("GC'ing {}", this); close(); super.finalize(); } /** * use {@link #builder()} to get an instance */ public static class Builder { public static final String RANDOM_PORT = "__RANDOM_PORT__"; private static final Pattern RANDOM_PORT_PATTERN = Pattern.compile(RANDOM_PORT); int[] nodes = { 1 }; private boolean start = true; private Boolean isDSE = null; private String version = getCassandraVersion(); private Set<String> createOptions = new LinkedHashSet<String>(getInstallArguments()); private Set<String> jvmArgs = new LinkedHashSet<String>(); private final Map<String, Object> cassandraConfiguration = Maps.newLinkedHashMap(); private final Map<String, Object> dseConfiguration = Maps.newLinkedHashMap(); private Map<Integer, Workload[]> workloads = new HashMap<Integer, Workload[]>(); private Builder() { cassandraConfiguration.put("start_rpc", false); cassandraConfiguration.put("storage_port", RANDOM_PORT); cassandraConfiguration.put("rpc_port", RANDOM_PORT); cassandraConfiguration.put("native_transport_port", RANDOM_PORT); } /** * Number of hosts for each DC. Defaults to [1] (1 DC with 1 node) */ public Builder withNodes(int... nodes) { this.nodes = nodes; return this; } public Builder withoutNodes() { return withNodes(); } /** * Enables SSL encryption. */ public Builder withSSL() { cassandraConfiguration.put("client_encryption_options.enabled", "true"); cassandraConfiguration.put("client_encryption_options.keystore", DEFAULT_SERVER_KEYSTORE_FILE.getAbsolutePath()); cassandraConfiguration.put("client_encryption_options.keystore_password", DEFAULT_SERVER_KEYSTORE_PASSWORD); return this; } /** * Enables client authentication. * This also enables encryption ({@link #withSSL()}. */ public Builder withAuth() { withSSL(); cassandraConfiguration.put("client_encryption_options.require_client_auth", "true"); cassandraConfiguration.put("client_encryption_options.truststore", DEFAULT_SERVER_TRUSTSTORE_FILE.getAbsolutePath()); cassandraConfiguration.put("client_encryption_options.truststore_password", DEFAULT_SERVER_TRUSTSTORE_PASSWORD); return this; } /** * Whether to start the cluster immediately (defaults to true if this is never called). */ public Builder notStarted() { this.start = false; return this; } /** * Sets this cluster to be a DSE cluster (defaults to {@link #isDSE()} if this is never called). */ public Builder withDSE() { this.isDSE = true; return this; } /** * Sets this cluster to be a non-DSE cluster (defaults to {@link #isDSE()} if this is never called). */ public Builder withoutDSE() { this.isDSE = false; return this; } /** * The Cassandra or DSE version to use (defaults to {@link #getCassandraVersion()} if this is never called). */ public Builder withVersion(String version) { Iterator<String> it = createOptions.iterator(); while (it.hasNext()) { String option = it.next(); // remove any version previously set and // install-dir, which is incompatible if (option.startsWith("-v ") || option.startsWith("--install-dir")) it.remove(); } this.createOptions.add("-v " + version); this.version = version; return this; } /** * Free-form options that will be added at the end of the {@code ccm create} command * (defaults to {@link #getInstallArguments()} if this is never called). */ public Builder withCreateOptions(String... createOptions) { Collections.addAll(this.createOptions, createOptions); return this; } /** * Customizes entries in cassandra.yaml (can be called multiple times) */ public Builder withCassandraConfiguration(String key, Object value) { this.cassandraConfiguration.put(key, value); return this; } /** * Customizes entries in dse.yaml (can be called multiple times) */ public Builder withDSEConfiguration(String key, Object value) { this.dseConfiguration.put(key, value); return this; } /** * JVM args to use when starting hosts. * System properties should be provided one by one, as a string in the form: * {@code -Dname=value}. */ public Builder withJvmArgs(String... jvmArgs) { Collections.addAll(this.jvmArgs, jvmArgs); return this; } public Builder withStoragePort(int port) { cassandraConfiguration.put("storage_port", port); return this; } public Builder withThriftPort(int port) { cassandraConfiguration.put("rpc_port", port); return this; } public Builder withBinaryPort(int port) { cassandraConfiguration.put("native_transport_port", port); return this; } /** * Sets the DSE workload for a given node. * * @param node The node to set the workload for (starting with 1). * @param workload The workload(s) (e.g. solr, spark, hadoop) * @return This builder */ public Builder withWorkload(int node, Workload... workload) { this.workloads.put(node, workload); return this; } public CCMBridge build() { // be careful NOT to alter internal state (hashCode/equals) during build! String clusterName = TestUtils.generateIdentifier("ccm_"); boolean dse = isDSE == null ? isDSE() : isDSE; Map<String, Object> cassandraConfiguration = randomizePorts(this.cassandraConfiguration); VersionNumber version = VersionNumber.parse(this.version); int storagePort = Integer.parseInt(cassandraConfiguration.get("storage_port").toString()); int thriftPort = Integer.parseInt(cassandraConfiguration.get("rpc_port").toString()); int binaryPort = Integer.parseInt(cassandraConfiguration.get("native_transport_port").toString()); final CCMBridge ccm = new CCMBridge(clusterName, dse, version, storagePort, thriftPort, binaryPort, joinJvmArgs(), nodes); Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { ccm.close(); } }); ccm.execute(buildCreateCommand(clusterName, dse)); updateNodeConf(ccm); ccm.updateConfig(cassandraConfiguration); if (dse) { Map<String, Object> dseConfiguration = Maps.newLinkedHashMap(this.dseConfiguration); /* TODO: Use version passed in if present, there is currently a conflation of C* and DSE versions * when it comes to this that don't want to disturb. No tests are currently using withVersion with * dse, so its not an issue at the moment. */ if (VersionNumber.parse(CCMBridge.getDSEVersion()).getMajor() >= 5) { // randomize DSE specific ports if dse present and greater than 5.0 dseConfiguration.put("lease_netty_server_port", RANDOM_PORT); dseConfiguration.put("internode_messaging_options.port", RANDOM_PORT); } dseConfiguration = randomizePorts(dseConfiguration); if (!dseConfiguration.isEmpty()) ccm.updateDSEConfig(dseConfiguration); } for (Map.Entry<Integer, Workload[]> entry : workloads.entrySet()) { ccm.setWorkload(entry.getKey(), entry.getValue()); } if (start) ccm.start(); return ccm; } public int weight() { // the weight is simply function of the number of nodes int totalNodes = 0; for (int nodesPerDc : this.nodes) { totalNodes += nodesPerDc; } return totalNodes; } private String joinJvmArgs() { StringBuilder allJvmArgs = new StringBuilder(""); String quote = isWindows() ? "\"" : ""; for (String jvmArg : jvmArgs) { // Windows requires jvm arguments to be quoted, while *nix requires unquoted. allJvmArgs.append(" "); allJvmArgs.append(quote); allJvmArgs.append("--jvm_arg="); allJvmArgs.append(randomizePorts(jvmArg)); allJvmArgs.append(quote); } return allJvmArgs.toString(); } private String buildCreateCommand(String clusterName, boolean dse) { StringBuilder result = new StringBuilder(CCM_COMMAND + " create"); result.append(" ").append(clusterName); result.append(" -i ").append(TestUtils.IP_PREFIX); result.append(" "); if (nodes.length > 0) { result.append(" -n "); for (int i = 0; i < nodes.length; i++) { int node = nodes[i]; if (i > 0) result.append(':'); result.append(node); } } // If not DSE, remove --dse if in createOptions. Set<String> lCreateOptions = new LinkedHashSet<String>(createOptions); if (!dse) { Iterator<String> it = lCreateOptions.iterator(); while (it.hasNext()) { String option = it.next(); // remove any version previously set and // install-dir, which is incompatible if (option.equals("--dse")) it.remove(); } } result.append(" ").append(Joiner.on(" ").join(randomizePorts(lCreateOptions))); return result.toString(); } /** * This is a workaround for an oddity in CCM: * when we create a cluster with -n option and * non-standard ports, the node.conf files are not updated accordingly. */ private void updateNodeConf(CCMBridge ccm) { int n = 1; Closer closer = Closer.create(); try { for (int dc = 1; dc <= nodes.length; dc++) { int nodesInDc = nodes[dc - 1]; for (int i = 0; i < nodesInDc; i++) { int jmxPort = findAvailablePort(); int debugPort = findAvailablePort(); logger.trace("Node {} in cluster {} using JMX port {} and debug port {}", n, ccm.getClusterName(), jmxPort, debugPort); File nodeConf = new File(ccm.getNodeDir(n), "node.conf"); File nodeConf2 = new File(ccm.getNodeDir(n), "node.conf.tmp"); BufferedReader br = closer.register(new BufferedReader(new FileReader(nodeConf))); PrintWriter pw = closer.register(new PrintWriter(new FileWriter(nodeConf2))); String line; while ((line = br.readLine()) != null) { line = line.replace("9042", Integer.toString(ccm.binaryPort)) .replace("9160", Integer.toString(ccm.thriftPort)) .replace("7000", Integer.toString(ccm.storagePort)); if (line.startsWith("jmx_port")) { line = String.format("jmx_port: '%s'", jmxPort); } else if (line.startsWith("remote_debug_port")) { line = String.format("remote_debug_port: %s:%s", TestUtils.ipOfNode(n), debugPort); } pw.println(line); } pw.flush(); pw.close(); Files.move(nodeConf2, nodeConf); n++; } } } catch (IOException e) { Throwables.propagate(e); } finally { try { closer.close(); } catch (IOException e) { Throwables.propagate(e); } } } private Set<String> randomizePorts(Set<String> set) { Set<String> randomized = new LinkedHashSet<String>(); for (String value : set) { randomized.add(randomizePorts(value)); } return randomized; } private Map<String, Object> randomizePorts(Map<String, Object> map) { Map<String, Object> randomized = new HashMap<String, Object>(); for (Map.Entry<String, Object> entry : map.entrySet()) { Object value = entry.getValue(); if (value instanceof CharSequence) { value = randomizePorts((CharSequence) value); } randomized.put(entry.getKey(), value); } return randomized; } private String randomizePorts(CharSequence str) { Matcher matcher = RANDOM_PORT_PATTERN.matcher(str); StringBuffer sb = new StringBuffer(); while (matcher.find()) { matcher.appendReplacement(sb, Integer.toString(TestUtils.findAvailablePort())); } matcher.appendTail(sb); return sb.toString(); } @Override @SuppressWarnings("SimplifiableIfStatement") public boolean equals(Object o) { // do not include cluster name and start, only // properties relevant to the settings of the cluster if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Builder builder = (Builder) o; if (!Arrays.equals(nodes, builder.nodes)) return false; if (isDSE != null ? !isDSE.equals(builder.isDSE) : builder.isDSE != null) return false; if (!version.equals(builder.version)) return false; if (!createOptions.equals(builder.createOptions)) return false; if (!jvmArgs.equals(builder.jvmArgs)) return false; if (!cassandraConfiguration.equals(builder.cassandraConfiguration)) return false; if (!dseConfiguration.equals(builder.dseConfiguration)) return false; return workloads.equals(builder.workloads); } @Override public int hashCode() { // do not include cluster name and start, only // properties relevant to the settings of the cluster int result = Arrays.hashCode(nodes); result = 31 * result + (isDSE != null ? isDSE.hashCode() : 0); result = 31 * result + createOptions.hashCode(); result = 31 * result + jvmArgs.hashCode(); result = 31 * result + cassandraConfiguration.hashCode(); result = 31 * result + dseConfiguration.hashCode(); result = 31 * result + workloads.hashCode(); result = 31 * result + version.hashCode(); return result; } } }