diff --git a/tools/jmeter/README.md b/tools/jmeter/README.md index 7166f8678f..6e67cce028 100644 --- a/tools/jmeter/README.md +++ b/tools/jmeter/README.md @@ -7,7 +7,7 @@ the Corda node via RPC. To run up the JMeter UI, using the jmeter.properties in the resources folder, type the following: -./gradlew tools:jmeter:run +`./gradlew tools:jmeter:run` You can then open the example script in "Example Flow Properties.jmx" via the File -> Open menu option. You need to configure the host, ports, user name and password in the Java Sampler that correspond to your chosen target Corda node. @@ -15,7 +15,7 @@ Simply running from the UI will result in the RPC client running inside the UI J If you wish to pass additional arguments to JMeter, you can do this: -./gradlew tools:jmeter:run -PjmeterArgs="['-n', '-Ljmeter.engine=DEBUG']" +`./gradlew tools:jmeter:run -PjmeterArgs="['-n', '-Ljmeter.engine=DEBUG']"` The intention is to run against a remote Corda node or nodes, hosted on servers rather than desktops. To this end, we leverage the JMeter ability to run remote agents that actually execute the tests, with these @@ -26,7 +26,7 @@ The remote agents then run close to the nodes, so the latency of RPC calls is mi A Capsule (http://www.capsule.io) based launchable JAR is created that can be run with the simple command line -java -jar jmeter-corda-.jar +`java -jar jmeter-corda-.jar` Embedded in the JAR is all of the corda code for flows and RPC, as well as the jmeter.propeties. This JAR will also include a properties file based on the hostname in the JMeter configuration, @@ -34,7 +34,7 @@ so we allocate different SSH tunneled port numbers this way. To launch JMeter with the tunnels automatically created: -./gradlew tools:jmeter:run -PjmeterHosts="['hostname1', 'hostname2']" +`./gradlew tools:jmeter:run -PjmeterHosts="['hostname1', 'hostname2']"` The list of hostnames should be of at least length one, with a maximum equal to the length of the remote_hosts option in jmeter.properties. We effectively "zip" together the hostnames and that list to build the SSH tunnels. @@ -45,4 +45,13 @@ from remote agents. The SSH tunnels can be started independently with: -./gradlew tools:jmeter:runSsh -PjmeterHosts="['hostname1', 'hostname2']" \ No newline at end of file +`./gradlew tools:jmeter:runSsh -PjmeterHosts="['hostname1', 'hostname2']"` + +For the ssh tunneling to work, an ssh agent must be running on your local machine with the +appropriate private key loaded. If the environment variable `SSH_AUTH_SOCK` is set, the code +assumes that a posix sshagent process is being used, if it is not set, it assumes that +[Pageant](https://www.ssh.com/ssh/putty/putty-manuals/0.68/Chapter9.html) is in use. If the +remote user name is different from the current user name, `-XsshUser ` +can be used to set this, or in the gradle call: + +`./gradlew tools:jmeter:runSsh -PjmeterHosts="['hostname1', 'hostname2']" -PsshUser="'username'"` diff --git a/tools/jmeter/build.gradle b/tools/jmeter/build.gradle index c4c16b0241..aa041346fc 100644 --- a/tools/jmeter/build.gradle +++ b/tools/jmeter/build.gradle @@ -13,6 +13,7 @@ dependencies { compile group: 'com.jcraft', name: 'jsch.agentproxy.core', version: '0.0.9' compile group: 'com.jcraft', name: 'jsch.agentproxy.sshagent', version: '0.0.9' compile group: 'com.jcraft', name: 'jsch.agentproxy.usocket-jna', version: '0.0.9' + compile group: 'com.jcraft', name: 'jsch.agentproxy.pageant', version: '0.0.9' // Log4J: logging framework (with SLF4J bindings) compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" @@ -60,8 +61,13 @@ task(runServer, dependsOn: 'classes', type: JavaExec) { task(runSsh, dependsOn: 'classes', type: JavaExec) { classpath = sourceSets.main.runtimeClasspath main = 'com.r3.corda.jmeter.Ssh' + if ( project.hasProperty("sshUser") ){ + args+= "-XsshUser" + args+= Eval.me(sshUser) + } if ( project.hasProperty("jmeterHosts") ) { - args Eval.me(jmeterHosts) + args+= "-Xssh" + args+= Eval.me(jmeterHosts) } standardInput = System.in } @@ -85,6 +91,10 @@ run { if ( project.hasProperty("jmeterArgs") ) { args+= Eval.me(jmeterHosts) } + if ( project.hasProperty("sshUser") ){ + args+= "-XsshUser" + args+= Eval.me(sshUser) + } if ( project.hasProperty("jmeterHosts") ) { args+= "-Xssh" args+= Eval.me(jmeterHosts) diff --git a/tools/jmeter/src/main/kotlin/com/r3/corda/jmeter/JshHelper.kt b/tools/jmeter/src/main/kotlin/com/r3/corda/jmeter/JshHelper.kt index d2f7ad3477..d92a965948 100644 --- a/tools/jmeter/src/main/kotlin/com/r3/corda/jmeter/JshHelper.kt +++ b/tools/jmeter/src/main/kotlin/com/r3/corda/jmeter/JshHelper.kt @@ -5,6 +5,7 @@ import com.jcraft.jsch.Identity import com.jcraft.jsch.IdentityRepository import com.jcraft.jsch.JSch import com.jcraft.jsch.agentproxy.AgentProxy +import com.jcraft.jsch.agentproxy.connector.PageantConnector import com.jcraft.jsch.agentproxy.connector.SSHAgentConnector import com.jcraft.jsch.agentproxy.usocket.JNAUSocketFactory import org.slf4j.LoggerFactory @@ -16,7 +17,11 @@ private val log = LoggerFactory.getLogger(Ssh::class.java) * Creates a new [JSch] instance with identities loaded from the running SSH agent. */ fun setupJSchWithSshAgent(): JSch { - val connector = SSHAgentConnector(JNAUSocketFactory()) + val connector = + if (System.getenv("SSH_AUTH_SOCK") == null) + PageantConnector() + else + SSHAgentConnector(JNAUSocketFactory()) val agentProxy = AgentProxy(connector) val identities = agentProxy.identities require(identities.isNotEmpty()) { "No SSH identities found, please add one to the agent" } diff --git a/tools/jmeter/src/main/kotlin/com/r3/corda/jmeter/Launcher.kt b/tools/jmeter/src/main/kotlin/com/r3/corda/jmeter/Launcher.kt index d1ebfcca23..b11d1d9f0d 100644 --- a/tools/jmeter/src/main/kotlin/com/r3/corda/jmeter/Launcher.kt +++ b/tools/jmeter/src/main/kotlin/com/r3/corda/jmeter/Launcher.kt @@ -1,5 +1,6 @@ package com.r3.corda.jmeter +import com.sun.javaws.exceptions.InvalidArgumentException import net.corda.core.internal.div import org.apache.jmeter.JMeter import org.slf4j.LoggerFactory @@ -54,19 +55,29 @@ class Launcher { } } - private fun maybeOpenSshTunnels(args: Array): Array { + fun maybeOpenSshTunnels(args: Array): Array { // We trim the args at the point "-Xssh" appears in the array of args. Anything after that is a host to - // SSH tunnel to. + // SSH tunnel to. Also get and remove the "-XsshUser" argument if it appears. var index = 0 - for (arg in args) { - if (arg == "-Xssh") { + var userName = System.getProperty("user.name") + val returnArgs = mutableListOf() + while (index < args.size) { + if (args[index] == "-XsshUser") { + ++index + if (index == args.size || args[index].startsWith("-")) { + throw InvalidArgumentException(args) + } + userName = args[index] + } else if (args[index] == "-Xssh") { // start ssh - Ssh.main(args.copyOfRange(index + 1, args.size), false) - return if (index == 0) emptyArray() else args.copyOfRange(0, index) + Ssh.createSshTunnels(args.copyOfRange(index + 1, args.size), userName, false) + return returnArgs.toTypedArray() + } else { + returnArgs.add(args[index]) } index++ } - return args + return returnArgs.toTypedArray() } } } \ No newline at end of file diff --git a/tools/jmeter/src/main/kotlin/com/r3/corda/jmeter/Ssh.kt b/tools/jmeter/src/main/kotlin/com/r3/corda/jmeter/Ssh.kt index 71eb682cbd..a01af325e5 100644 --- a/tools/jmeter/src/main/kotlin/com/r3/corda/jmeter/Ssh.kt +++ b/tools/jmeter/src/main/kotlin/com/r3/corda/jmeter/Ssh.kt @@ -2,6 +2,7 @@ package com.r3.corda.jmeter import com.jcraft.jsch.JSch import com.jcraft.jsch.Session +import com.sun.javaws.exceptions.InvalidArgumentException import net.corda.core.utilities.NetworkHostAndPort import net.corda.nodeapi.internal.addShutdownHook import org.slf4j.LoggerFactory @@ -17,9 +18,30 @@ class Ssh { val log = LoggerFactory.getLogger(this::class.java) @JvmStatic - @JvmOverloads - fun main(args: Array, wait: Boolean = true) { - val userName = System.getProperty("user.name") + fun main(args: Array) { + // parse the args and call createSshTunnels + // the only arguments recognised are "-XsshUser" for the remote user name + // and "-Xssh" - everything after this will be treated as a remote host name + var userName = System.getProperty("user.name") + var index = 0 + while (index < args.size) { + if (args[index] == "-XsshUser") { + ++index + if (index == args.size || args[index].startsWith("-")) { + throw InvalidArgumentException(args) + } + userName = args[index] + } else if (args[index] == "-Xssh") { + createSshTunnels(args.copyOfRange(index + 1, args.size), userName, true) + return + } + } + log.info("Nothing to be done - did you specify hosts to tunnel to with -Xssh?") + } + + + fun createSshTunnels(hosts: Array, userName: String, wait: Boolean) { + log.info("User name for ssh: ${userName}") val jsch = setupJSchWithSshAgent() val sessions = mutableListOf() @@ -34,7 +56,7 @@ class Ssh { // Where JMeter driver will try to connect for remote agents (should all be localhost so can ssh tunnel). val localHostsAndPorts = jmeterProps.getProperty("remote_hosts", "").split(',').map { it.trim() } - args.zip(localHostsAndPorts) { remoteHost, localHostAndPortString -> + hosts.zip(localHostsAndPorts) { remoteHost, localHostAndPortString -> // Actual remote host and port we will tunnel to. log.info("Creating tunnels for $remoteHost") val localHostAndPort = NetworkHostAndPort.parse(localHostAndPortString)