From 046b104fee70a56402faac64d527c8df1193e61f Mon Sep 17 00:00:00 2001 From: Anthony Keenan Date: Thu, 13 Sep 2018 11:14:31 +0100 Subject: [PATCH] CORDA-1764: Make shell use picocli for parsing command line options (#3923) * Fix link in shell documentation * The TypeSafe config parser wants extensions.sshd to be present in the config even though extensions is nullable * Temp commit * Make Standalone Shell use picocli * Simplify gradle config for bootstrapper * Fix logging dependency issues * Revert "Temp commit" This reverts commit f4efafcc9dce3a8fa26a270e3d8f34b282c8d24b. * Fix quasarExcludeExpression * Correct bootstrapper configuration * Correct CRaSH capitalisation in docs * Fix unit tests * Fix help text typo * Make logging level case insensitive * Fix CRaSH capitalisation in help text * Fix unit tests --- docs/source/shell.rst | 2 +- node/capsule/build.gradle | 2 +- .../net/corda/node/internal/NodeStartup.kt | 2 +- .../net/corda/node/NodeCmdLineOptionsTest.kt | 2 +- tools/bootstrapper/build.gradle | 13 +- .../src/main/resources/log4j2.xml | 16 -- tools/cliutils/build.gradle | 2 +- .../net/corda/cliutils/CordaCliWrapper.kt | 10 +- tools/shell-cli/README.md | 2 +- tools/shell-cli/build.gradle | 4 +- .../corda/tools/shell/ShellCmdLineOptions.kt | 195 +++++++++++++++++ .../net/corda/tools/shell/StandaloneShell.kt | 64 +++--- .../tools/shell/StandaloneShellArgsParser.kt | 207 ------------------ .../shell/StandaloneShellArgsParserTest.kt | 154 ++++--------- 14 files changed, 297 insertions(+), 378 deletions(-) delete mode 100644 tools/bootstrapper/src/main/resources/log4j2.xml create mode 100644 tools/shell-cli/src/main/kotlin/net/corda/tools/shell/ShellCmdLineOptions.kt delete mode 100644 tools/shell-cli/src/main/kotlin/net/corda/tools/shell/StandaloneShellArgsParser.kt diff --git a/docs/source/shell.rst b/docs/source/shell.rst index 73ef8f0a6d..a1051c3c77 100644 --- a/docs/source/shell.rst +++ b/docs/source/shell.rst @@ -125,7 +125,7 @@ Where: * ``config-file`` is the path to config file, used instead of providing the rest of command line options * ``cordpass-directory`` is the directory containing Cordapps jars, Cordapps are require when starting flows -* ``commands-directory`` is the directory with additional CrAsH shell commands +* ``commands-directory`` is the directory with additional CRaSH shell commands * ``host`` is the Corda node's host * ``port`` is the Corda node's port, specified in the ``node.conf`` file * ``user`` is the RPC username, if not provided it will be requested at startup diff --git a/node/capsule/build.gradle b/node/capsule/build.gradle index af22662a4e..d447926e41 100644 --- a/node/capsule/build.gradle +++ b/node/capsule/build.gradle @@ -46,7 +46,7 @@ task buildCordaJAR(type: FatCapsule, dependsOn: project(':node').tasks.jar) { applicationVersion = corda_release_version // See experimental/quasar-hook/README.md for how to generate. - def quasarExcludeExpression = "x(antlr**;bftsmart**;ch**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;com.fasterxml**;com.google**;com.ibm**;com.intellij**;com.jcabi**;com.nhaarman**;com.opengamma**;com.typesafe**;com.zaxxer**;de.javakaffee**;groovy**;groovyjarjarantlr**;groovyjarjarasm**;io.atomix**;io.github**;io.netty**;jdk**;joptsimple**;junit**;kotlin**;net.bytebuddy**;net.i2p**;org.apache**;org.assertj**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**;org.jolokia**)" + def quasarExcludeExpression = "x(antlr**;bftsmart**;ch**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;com.fasterxml**;com.google**;com.ibm**;com.intellij**;com.jcabi**;com.nhaarman**;com.opengamma**;com.typesafe**;com.zaxxer**;de.javakaffee**;groovy**;groovyjarjarantlr**;groovyjarjarasm**;io.atomix**;io.github**;io.netty**;jdk**;junit**;kotlin**;net.bytebuddy**;net.i2p**;org.apache**;org.assertj**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**;org.jolokia**)" javaAgents = ["quasar-core-${quasar_version}-jdk8.jar=${quasarExcludeExpression}"] systemProperties['visualvm.display.name'] = 'Corda' minJavaVersion = '1.8.0' diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt index 6da2c527e9..e04edeac2f 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -456,7 +456,7 @@ open class NodeStartup: CordaCliWrapper("corda", "Runs a Corda Node") { } override fun initLogging() { - val loggingLevel = loggingLevel.name().toLowerCase(Locale.ENGLISH) + val loggingLevel = loggingLevel.name.toLowerCase(Locale.ENGLISH) System.setProperty("defaultLogLevel", loggingLevel) // These properties are referenced from the XML config file. if (verbose) { System.setProperty("consoleLogLevel", loggingLevel) diff --git a/node/src/test/kotlin/net/corda/node/NodeCmdLineOptionsTest.kt b/node/src/test/kotlin/net/corda/node/NodeCmdLineOptionsTest.kt index 2741d9c2c3..1ee0e163ea 100644 --- a/node/src/test/kotlin/net/corda/node/NodeCmdLineOptionsTest.kt +++ b/node/src/test/kotlin/net/corda/node/NodeCmdLineOptionsTest.kt @@ -3,10 +3,10 @@ package net.corda.node import net.corda.core.internal.div import net.corda.node.internal.NodeStartup import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy -import org.apache.logging.log4j.Level import org.assertj.core.api.Assertions.assertThat import org.junit.BeforeClass import org.junit.Test +import org.slf4j.event.Level import java.nio.file.Path import java.nio.file.Paths diff --git a/tools/bootstrapper/build.gradle b/tools/bootstrapper/build.gradle index de04e80796..48e2cb50be 100644 --- a/tools/bootstrapper/build.gradle +++ b/tools/bootstrapper/build.gradle @@ -1,4 +1,3 @@ -apply plugin: 'java' apply plugin: 'kotlin' apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'com.jfrog.artifactory' @@ -8,14 +7,15 @@ description 'Network bootstrapper' dependencies { compile project(':node-api') compile project(':tools:cliutils') - compile "info.picocli:picocli:$picocli_version" - compile "org.slf4j:jul-to-slf4j:$slf4j_version" compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" - compile "com.jcabi:jcabi-manifests:$jcabi_manifests_version" +} + +processResources { + from file("$rootDir/config/dev/log4j2.xml") } jar { - from(configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }) { + from(configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }) { exclude "META-INF/*.SF" exclude "META-INF/*.DSA" exclude "META-INF/*.RSA" @@ -23,10 +23,9 @@ jar { from(project(':node:capsule').tasks['buildCordaJAR']) { rename 'corda-(.*)', 'corda.jar' } - archiveName = "network-bootstrapper-${corda_release_version}.jar" + baseName = "network-bootstrapper" manifest { attributes( - 'Automatic-Module-Name': 'net.corda.bootstrapper', 'Main-Class': 'net.corda.bootstrapper.MainKt' ) } diff --git a/tools/bootstrapper/src/main/resources/log4j2.xml b/tools/bootstrapper/src/main/resources/log4j2.xml deleted file mode 100644 index 98b3648e6b..0000000000 --- a/tools/bootstrapper/src/main/resources/log4j2.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - off - - - - - - - - - - - - \ No newline at end of file diff --git a/tools/cliutils/build.gradle b/tools/cliutils/build.gradle index 5537674930..8bd2e15687 100644 --- a/tools/cliutils/build.gradle +++ b/tools/cliutils/build.gradle @@ -10,7 +10,7 @@ dependencies { compile "info.picocli:picocli:$picocli_version" compile "com.jcabi:jcabi-manifests:$jcabi_manifests_version" - compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" + compile "org.slf4j:slf4j-api:$slf4j_version" // JAnsi: for drawing things to the terminal in nicely coloured ways. compile "org.fusesource.jansi:jansi:$jansi_version" diff --git a/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt b/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt index 1b9adcf04f..8d37b5f694 100644 --- a/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt +++ b/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt @@ -4,10 +4,11 @@ import net.corda.core.internal.rootMessage import net.corda.core.utilities.contextLogger import net.corda.core.utilities.loggerFor -import org.apache.logging.log4j.Level import org.fusesource.jansi.AnsiConsole +import org.slf4j.event.Level import picocli.CommandLine import picocli.CommandLine.* +import java.nio.file.Paths import kotlin.system.exitProcess import java.util.* import java.util.concurrent.Callable @@ -127,11 +128,12 @@ abstract class CordaCliWrapper(val alias: String, val description: String) : Cal // This needs to be called before loggers (See: NodeStartup.kt:51 logger called by lazy, initLogging happens before). // Node's logging is more rich. In corda configurations two properties, defaultLoggingLevel and consoleLogLevel, are usually used. open fun initLogging() { - val loggingLevel = loggingLevel.name().toLowerCase(Locale.ENGLISH) + val loggingLevel = loggingLevel.name.toLowerCase(Locale.ENGLISH) System.setProperty("defaultLogLevel", loggingLevel) // These properties are referenced from the XML config file. if (verbose) { System.setProperty("consoleLogLevel", loggingLevel) } + System.setProperty("log-path", Paths.get(".").toString()) } // Override this function with the actual method to be run once all the arguments have been parsed. The return number @@ -147,11 +149,11 @@ abstract class CordaCliWrapper(val alias: String, val description: String) : Cal } /** - * Converter from String to log4j logging Level. + * Converter from String to slf4j logging Level. */ class LoggingLevelConverter : ITypeConverter { override fun convert(value: String?): Level { - return value?.let { Level.getLevel(it) } + return value?.let { Level.valueOf(it.toUpperCase()) } ?: throw TypeConversionException("Unknown option for --logging-level: $value") } diff --git a/tools/shell-cli/README.md b/tools/shell-cli/README.md index c6a00181cd..131fa0ba66 100644 --- a/tools/shell-cli/README.md +++ b/tools/shell-cli/README.md @@ -1,7 +1,7 @@ Standalone Shell ---------------- -Documentation for shell CLI can be found [here](http://docs.corda.net/website/releases/docs_head/shell.html) +Documentation for the standalone shell can be found [here](https://docs.corda.net/head/shell.html#the-standalone-shell) To build this from the command line on Unix or MacOS: diff --git a/tools/shell-cli/build.gradle b/tools/shell-cli/build.gradle index 89a74f394c..6cafc920fa 100644 --- a/tools/shell-cli/build.gradle +++ b/tools/shell-cli/build.gradle @@ -10,7 +10,9 @@ apply plugin: 'com.jfrog.artifactory' dependencies { compile project(':tools:shell') - compile "org.slf4j:slf4j-simple:$slf4j_version" + compile project(':tools:cliutils') + compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" + compile "org.slf4j:jul-to-slf4j:$slf4j_version" testCompile(project(':test-utils')) { exclude group: 'org.apache.logging.log4j', module: 'log4j-slf4j-impl' diff --git a/tools/shell-cli/src/main/kotlin/net/corda/tools/shell/ShellCmdLineOptions.kt b/tools/shell-cli/src/main/kotlin/net/corda/tools/shell/ShellCmdLineOptions.kt new file mode 100644 index 0000000000..35da8b0cf0 --- /dev/null +++ b/tools/shell-cli/src/main/kotlin/net/corda/tools/shell/ShellCmdLineOptions.kt @@ -0,0 +1,195 @@ +package net.corda.tools.shell + +import com.typesafe.config.Config +import com.typesafe.config.ConfigFactory +import net.corda.core.internal.div +import net.corda.core.messaging.ClientRpcSslOptions +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.nodeapi.internal.config.parseAs +import net.corda.tools.shell.ShellConfiguration.Companion.COMMANDS_DIR +import picocli.CommandLine.Option +import java.nio.file.Path +import java.nio.file.Paths + +class ShellCmdLineOptions { + @Option( + names = ["-f", "--config-file"], + description = ["The path to the shell configuration file, used instead of providing the rest of command line options."] + ) + var configFile: Path? = null + + @Option( + names = ["-c", "--cordapp-directory"], + description = ["The path to the directory containing CorDapp JARs, CorDapps are required when starting flows."] + ) + var cordappDirectory: Path? = null + + @Option( + names = ["-o", "--commands-directory"], + description = ["The path to the directory containing additional CRaSH shell commands."] + ) + var commandsDirectory: Path? = null + + @Option( + names = ["-a", "--host"], + description = ["The host address of the Corda node."] + ) + var host: String? = null + + @Option( + names = ["-p", "--port"], + description = ["The RPC port of the Corda node."] + ) + var port: String? = null + + @Option( + names = ["--user"], + description = ["The RPC user name."] + ) + var user: String? = null + + @Option( + names = ["--password"], + description = ["The RPC user password."] + ) + var password: String? = null + + + @Option( + names = ["--sshd-port"], + description = ["Enables SSH server for shell."] + ) + var sshdPort: String? = null + + @Option( + names = ["--sshd-hostkey-directory"], + description = ["The directory with hostkey.pem file for SSH server."] + ) + var sshdHostKeyDirectory: Path? = null + + @Option( + names = ["--truststore-password"], + description = ["The password to unlock the TrustStore file."] + ) + var trustStorePassword: String? = null + + + @Option( + names = ["--truststore-file"], + description = ["The path to the TrustStore file."] + ) + var trustStoreFile: Path? = null + + + @Option( + names = ["--truststore-type"], + description = ["The type of the TrustStore (e.g. JKS)."] + ) + var trustStoreType: String? = null + + + private fun toConfigFile(): Config { + val cmdOpts = mutableMapOf() + + commandsDirectory?.apply { cmdOpts["extensions.commands.path"] = this.toString() } + cordappDirectory?.apply { cmdOpts["extensions.cordapps.path"] = this.toString() } + user?.apply { cmdOpts["node.user"] = this } + password?.apply { cmdOpts["node.password"] = this } + host?.apply { cmdOpts["node.addresses.rpc.host"] = this } + port?.apply { cmdOpts["node.addresses.rpc.port"] = this } + trustStoreFile?.apply { cmdOpts["ssl.truststore.path"] = this.toString() } + trustStorePassword?.apply { cmdOpts["ssl.truststore.password"] = this } + trustStoreType?.apply { cmdOpts["ssl.truststore.type"] = this } + sshdPort?.apply { + cmdOpts["extensions.sshd.port"] = this + cmdOpts["extensions.sshd.enabled"] = true + } + sshdHostKeyDirectory?.apply { cmdOpts["extensions.sshd.hostkeypath"] = this.toString() } + + return ConfigFactory.parseMap(cmdOpts) + } + + /** Return configuration parsed from an optional config file (provided by the command line option) + * and then overridden by the command line options */ + fun toConfig(): ShellConfiguration { + val fileConfig = configFile?.let { ConfigFactory.parseFile(it.toFile()) } + ?: ConfigFactory.empty() + val typeSafeConfig = toConfigFile().withFallback(fileConfig).resolve() + val shellConfigFile = typeSafeConfig.parseAs() + return shellConfigFile.toShellConfiguration() + } +} + +/** Object representation of Shell configuration file */ +private class ShellConfigurationFile { + data class Rpc( + val host: String, + val port: Int) + + data class Addresses( + val rpc: Rpc + ) + + data class Node( + val addresses: Addresses, + val user: String?, + val password: String? + ) + + data class Cordapps( + val path: String + ) + + data class Sshd( + val enabled: Boolean, + val port: Int, + val hostkeypath: String? + ) + + data class Commands( + val path: String + ) + + data class Extensions( + val cordapps: Cordapps?, + val sshd: Sshd?, + val commands: Commands? + ) + + data class KeyStore( + val path: String, + val type: String = "JKS", + val password: String + ) + + data class Ssl( + val truststore: KeyStore + ) + + data class ShellConfigFile( + val node: Node, + val extensions: Extensions?, + val ssl: Ssl? + ) { + fun toShellConfiguration(): ShellConfiguration { + + val sslOptions = + ssl?.let { + ClientRpcSslOptions( + trustStorePath = Paths.get(it.truststore.path), + trustStorePassword = it.truststore.password) + } + + return ShellConfiguration( + commandsDirectory = extensions?.commands?.let { Paths.get(it.path) } ?: Paths.get(".") + / COMMANDS_DIR, + cordappsDirectory = extensions?.cordapps?.let { Paths.get(it.path) }, + user = node.user ?: "", + password = node.password ?: "", + hostAndPort = NetworkHostAndPort(node.addresses.rpc.host, node.addresses.rpc.port), + ssl = sslOptions, + sshdPort = extensions?.sshd?.let { if (it.enabled) it.port else null }, + sshHostKeyDirectory = extensions?.sshd?.let { if (it.enabled && it.hostkeypath != null) Paths.get(it.hostkeypath) else null }) + } + } +} diff --git a/tools/shell-cli/src/main/kotlin/net/corda/tools/shell/StandaloneShell.kt b/tools/shell-cli/src/main/kotlin/net/corda/tools/shell/StandaloneShell.kt index 00f8928b81..8d40c858ea 100644 --- a/tools/shell-cli/src/main/kotlin/net/corda/tools/shell/StandaloneShell.kt +++ b/tools/shell-cli/src/main/kotlin/net/corda/tools/shell/StandaloneShell.kt @@ -1,45 +1,34 @@ package net.corda.tools.shell import com.jcabi.manifests.Manifests -import joptsimple.OptionException -import net.corda.core.internal.* +import net.corda.cliutils.CordaCliWrapper +import net.corda.cliutils.ExitCodes +import net.corda.cliutils.start +import net.corda.core.internal.exists +import net.corda.core.internal.isRegularFile +import net.corda.core.internal.list import org.fusesource.jansi.Ansi import org.fusesource.jansi.AnsiConsole +import org.slf4j.bridge.SLF4JBridgeHandler +import picocli.CommandLine.Mixin +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStreamReader import java.net.URL import java.net.URLClassLoader import java.nio.file.Path import java.util.concurrent.CountDownLatch import kotlin.streams.toList -import java.io.IOException -import java.io.BufferedReader -import java.io.InputStreamReader -import kotlin.system.exitProcess fun main(args: Array) { - - val argsParser = CommandLineOptionParser() - val cmdlineOptions = try { - argsParser.parse(*args) - } catch (e: OptionException) { - println("Invalid command line arguments: ${e.message}") - argsParser.printHelp(System.out) - exitProcess(1) - } - - if (cmdlineOptions.help) { - argsParser.printHelp(System.out) - return - } - val config = try { - cmdlineOptions.toConfig() - } catch(e: Exception) { - println("Configuration exception: ${e.message}") - exitProcess(1) - } - StandaloneShell(config).run() + StandaloneShell().start(args) } -class StandaloneShell(private val configuration: ShellConfiguration) { +class StandaloneShell : CordaCliWrapper("corda-shell", "The Corda standalone shell.") { + @Mixin + var cmdLineOptions = ShellCmdLineOptions() + + lateinit var configuration: ShellConfiguration private fun getCordappsInDirectory(cordappsDir: Path?): List = if (cordappsDir == null || !cordappsDir.exists()) { @@ -67,7 +56,20 @@ class StandaloneShell(private val configuration: ShellConfiguration) { private fun getManifestEntry(key: String) = if (Manifests.exists(key)) Manifests.read(key) else "Unknown" - fun run() { + override fun initLogging() { + super.initLogging() + SLF4JBridgeHandler.removeHandlersForRootLogger() // The default j.u.l config adds a ConsoleHandler. + SLF4JBridgeHandler.install() + } + + override fun runProgram(): Int { + configuration = try { + cmdLineOptions.toConfig() + } catch(e: Exception) { + println("Configuration exception: ${e.message}") + return ExitCodes.FAILURE + } + val cordappJarPaths = getCordappsInDirectory(configuration.cordappsDirectory) val classLoader: ClassLoader = URLClassLoader(cordappJarPaths.toTypedArray(), javaClass.classLoader) with(configuration) { @@ -84,7 +86,7 @@ class StandaloneShell(private val configuration: ShellConfiguration) { InteractiveShell.nodeInfo() } catch (e: Exception) { println("Cannot login to ${configuration.hostAndPort}, reason: \"${e.message}\"") - exitProcess(1) + return ExitCodes.FAILURE } val exit = CountDownLatch(1) @@ -106,6 +108,6 @@ class StandaloneShell(private val configuration: ShellConfiguration) { exit.await() // because we can't clean certain Crash Shell threads that block on read() - exitProcess(0) + return ExitCodes.SUCCESS } } diff --git a/tools/shell-cli/src/main/kotlin/net/corda/tools/shell/StandaloneShellArgsParser.kt b/tools/shell-cli/src/main/kotlin/net/corda/tools/shell/StandaloneShellArgsParser.kt deleted file mode 100644 index 9b628948e9..0000000000 --- a/tools/shell-cli/src/main/kotlin/net/corda/tools/shell/StandaloneShellArgsParser.kt +++ /dev/null @@ -1,207 +0,0 @@ -package net.corda.tools.shell - -import com.typesafe.config.Config -import com.typesafe.config.ConfigFactory -import joptsimple.OptionParser -import joptsimple.util.EnumConverter -import net.corda.core.internal.div -import net.corda.core.utilities.NetworkHostAndPort -import net.corda.core.messaging.ClientRpcSslOptions -import net.corda.nodeapi.internal.config.parseAs -import net.corda.tools.shell.ShellConfiguration.Companion.COMMANDS_DIR -import org.slf4j.event.Level -import java.io.PrintStream -import java.nio.file.Path -import java.nio.file.Paths - -// NOTE: Do not use any logger in this class as args parsing is done before the logger is setup. -class CommandLineOptionParser { - private val optionParser = OptionParser() - - private val configFileArg = optionParser - .accepts("config-file", "The path to the shell configuration file, used instead of providing the rest of command line options.") - .withOptionalArg() - private val cordappsDirectoryArg = optionParser - .accepts("cordpass-directory", "The path to directory containing Cordapps jars, Cordapps are require when starting flows.") - .withOptionalArg() - private val commandsDirectoryArg = optionParser - .accepts("commands-directory", "The directory with additional CrAsH shell commands.") - .withOptionalArg() - private val hostArg = optionParser - .acceptsAll(listOf("h", "host"), "The host of the Corda node.") - .withRequiredArg() - private val portArg = optionParser - .acceptsAll(listOf("p", "port"), "The port of the Corda node.") - .withRequiredArg() - private val userArg = optionParser - .accepts("user", "The RPC user name.") - .withOptionalArg() - private val passwordArg = optionParser - .accepts("password", "The RPC user password.") - .withOptionalArg() - private val loggerLevel = optionParser - .accepts("logging-level", "Enable logging at this level and higher.") - .withRequiredArg() - .withValuesConvertedBy(object : EnumConverter(Level::class.java) {}) - .defaultsTo(Level.INFO) - private val sshdPortArg = optionParser - .accepts("sshd-port", "Enables SSH server for shell.") - .withOptionalArg() - private val sshdHostKeyDirectoryArg = optionParser - .accepts("sshd-hostkey-directory", "The directory with hostkey.pem file for SSH server.") - .withOptionalArg() - private val helpArg = optionParser - .accepts("help") - .forHelp() - private val trustStorePasswordArg = optionParser - .accepts("truststore-password", "The password to unlock the TrustStore file.") - .withOptionalArg() - private val trustStoreDirArg = optionParser - .accepts("truststore-file", "The path to the TrustStore file.") - .withOptionalArg() - private val trustStoreTypeArg = optionParser - .accepts("truststore-type", "The type of the TrustStore (e.g. JKS).") - .withOptionalArg() - - fun parse(vararg args: String): CommandLineOptions { - val optionSet = optionParser.parse(*args) - return CommandLineOptions( - configFile = optionSet.valueOf(configFileArg), - host = optionSet.valueOf(hostArg), - port = optionSet.valueOf(portArg), - user = optionSet.valueOf(userArg), - password = optionSet.valueOf(passwordArg), - commandsDirectory = (optionSet.valueOf(commandsDirectoryArg))?.let { Paths.get(it).normalize().toAbsolutePath() }, - cordappsDirectory = (optionSet.valueOf(cordappsDirectoryArg))?.let { Paths.get(it).normalize().toAbsolutePath() }, - help = optionSet.has(helpArg), - loggingLevel = optionSet.valueOf(loggerLevel), - sshdPort = optionSet.valueOf(sshdPortArg), - sshdHostKeyDirectory = (optionSet.valueOf(sshdHostKeyDirectoryArg))?.let { Paths.get(it).normalize().toAbsolutePath() }, - trustStorePassword = optionSet.valueOf(trustStorePasswordArg), - trustStoreFile = (optionSet.valueOf(trustStoreDirArg))?.let { Paths.get(it).normalize().toAbsolutePath() }, - trustStoreType = optionSet.valueOf(trustStoreTypeArg)) - } - - fun printHelp(sink: PrintStream) = optionParser.printHelpOn(sink) -} - -data class CommandLineOptions(val configFile: String?, - val commandsDirectory: Path?, - val cordappsDirectory: Path?, - val host: String?, - val port: String?, - val user: String?, - val password: String?, - val help: Boolean, - val loggingLevel: Level, - val sshdPort: String?, - val sshdHostKeyDirectory: Path?, - val trustStorePassword: String?, - val trustStoreFile: Path?, - val trustStoreType: String?) { - - private fun toConfigFile(): Config { - val cmdOpts = mutableMapOf() - - commandsDirectory?.apply { cmdOpts["extensions.commands.path"] = this.toString() } - cordappsDirectory?.apply { cmdOpts["extensions.cordapps.path"] = this.toString() } - user?.apply { cmdOpts["node.user"] = this } - password?.apply { cmdOpts["node.password"] = this } - host?.apply { cmdOpts["node.addresses.rpc.host"] = this } - port?.apply { cmdOpts["node.addresses.rpc.port"] = this } - trustStoreFile?.apply { cmdOpts["ssl.truststore.path"] = this.toString() } - trustStorePassword?.apply { cmdOpts["ssl.truststore.password"] = this } - trustStoreType?.apply { cmdOpts["ssl.truststore.type"] = this } - sshdPort?.apply { - cmdOpts["extensions.sshd.port"] = this - cmdOpts["extensions.sshd.enabled"] = true - } - sshdHostKeyDirectory?.apply { cmdOpts["extensions.sshd.hostkeypath"] = this.toString() } - - return ConfigFactory.parseMap(cmdOpts) - } - - /** Return configuration parsed from an optional config file (provided by the command line option) - * and then overridden by the command line options */ - fun toConfig(): ShellConfiguration { - val fileConfig = configFile?.let { ConfigFactory.parseFile(Paths.get(configFile).toFile()) } - ?: ConfigFactory.empty() - val typeSafeConfig = toConfigFile().withFallback(fileConfig).resolve() - val shellConfigFile = typeSafeConfig.parseAs() - return shellConfigFile.toShellConfiguration() - } -} - -/** Object representation of Shell configuration file */ -private class ShellConfigurationFile { - data class Rpc( - val host: String, - val port: Int) - - data class Addresses( - val rpc: Rpc - ) - - data class Node( - val addresses: Addresses, - val user: String?, - val password: String? - ) - - data class Cordapps( - val path: String - ) - - data class Sshd( - val enabled: Boolean, - val port: Int, - val hostkeypath: String? - ) - - data class Commands( - val path: String - ) - - data class Extensions( - val cordapps: Cordapps, - val sshd: Sshd, - val commands: Commands? - ) - - data class KeyStore( - val path: String, - val type: String = "JKS", - val password: String - ) - - data class Ssl( - val truststore: KeyStore - ) - - data class ShellConfigFile( - val node: Node, - val extensions: Extensions?, - val ssl: Ssl? - ) { - fun toShellConfiguration(): ShellConfiguration { - - val sslOptions = - ssl?.let { - ClientRpcSslOptions( - trustStorePath = Paths.get(it.truststore.path), - trustStorePassword = it.truststore.password) - } - - return ShellConfiguration( - commandsDirectory = extensions?.commands?.let { Paths.get(it.path) } ?: Paths.get(".") - / COMMANDS_DIR, - cordappsDirectory = extensions?.cordapps?.let { Paths.get(it.path) }, - user = node.user ?: "", - password = node.password ?: "", - hostAndPort = NetworkHostAndPort(node.addresses.rpc.host, node.addresses.rpc.port), - ssl = sslOptions, - sshdPort = extensions?.sshd?.let { if (it.enabled) it.port else null }, - sshHostKeyDirectory = extensions?.sshd?.let { if (it.enabled && it.hostkeypath != null) Paths.get(it.hostkeypath) else null }) - } - } -} diff --git a/tools/shell-cli/src/test/kotlin/net/corda/tools/shell/StandaloneShellArgsParserTest.kt b/tools/shell-cli/src/test/kotlin/net/corda/tools/shell/StandaloneShellArgsParserTest.kt index d1817a822a..8563e65f26 100644 --- a/tools/shell-cli/src/test/kotlin/net/corda/tools/shell/StandaloneShellArgsParserTest.kt +++ b/tools/shell-cli/src/test/kotlin/net/corda/tools/shell/StandaloneShellArgsParserTest.kt @@ -1,97 +1,44 @@ package net.corda.tools.shell import net.corda.core.internal.toPath -import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.messaging.ClientRpcSslOptions -import org.assertj.core.api.Assertions.assertThat +import net.corda.core.utilities.NetworkHostAndPort import org.junit.Test -import org.slf4j.event.Level import java.nio.file.Paths import kotlin.test.assertEquals class StandaloneShellArgsParserTest { private val CONFIG_FILE = StandaloneShellArgsParserTest::class.java.getResource("/config.conf").toPath() - @Test - fun args_to_cmd_options() { - val args = arrayOf("--config-file", "/x/y/z/config.conf", - "--commands-directory", "/x/y/commands", - "--cordpass-directory", "/x/y/cordapps", - "--host", "alocalhost", - "--port", "1234", - "--user", "demo", - "--password", "abcd1234", - "--logging-level", "DEBUG", - "--sshd-port", "2223", - "--sshd-hostkey-directory", "/x/y/ssh", - "--help", - "--truststore-password", "pass2", - "--truststore-file", "/x/y/truststore.jks", - "--truststore-type", "dummy") - - val expectedOptions = CommandLineOptions( - configFile = "/x/y/z/config.conf", - commandsDirectory = Paths.get("/x/y/commands").normalize().toAbsolutePath(), - cordappsDirectory = Paths.get("/x/y/cordapps").normalize().toAbsolutePath(), - host = "alocalhost", - port = "1234", - user = "demo", - password = "abcd1234", - help = true, - loggingLevel = Level.DEBUG, - sshdPort = "2223", - sshdHostKeyDirectory = Paths.get("/x/y/ssh").normalize().toAbsolutePath(), - trustStorePassword = "pass2", - trustStoreFile = Paths.get("/x/y/truststore.jks").normalize().toAbsolutePath(), - trustStoreType = "dummy") - - val options = CommandLineOptionParser().parse(*args) - - assertThat(options).isEqualTo(expectedOptions) - } - @Test fun empty_args_to_cmd_options() { - val args = emptyArray() + val expectedOptions = ShellCmdLineOptions() - val expectedOptions = CommandLineOptions(configFile = null, - commandsDirectory = null, - cordappsDirectory = null, - host = null, - port = null, - user = null, - password = null, - help = false, - loggingLevel = Level.INFO, - sshdPort = null, - sshdHostKeyDirectory = null, - trustStorePassword = null, - trustStoreFile = null, - trustStoreType = null) - - val options = CommandLineOptionParser().parse(*args) - - assertEquals(expectedOptions, options) + assertEquals(expectedOptions.configFile, null) + assertEquals(expectedOptions.cordappDirectory, null) + assertEquals(expectedOptions.commandsDirectory, null) + assertEquals(expectedOptions.host, null) + assertEquals(expectedOptions.port, null) + assertEquals(expectedOptions.user, null) + assertEquals(expectedOptions.password, null) + assertEquals(expectedOptions.sshdPort, null) } @Test fun args_to_config() { - - val options = CommandLineOptions(configFile = null, - commandsDirectory = Paths.get("/x/y/commands"), - cordappsDirectory = Paths.get("/x/y/cordapps"), - host = "alocalhost", - port = "1234", - user = "demo", - password = "abcd1234", - help = true, - loggingLevel = Level.DEBUG, - sshdPort = "2223", - sshdHostKeyDirectory = Paths.get("/x/y/ssh"), - trustStorePassword = "pass2", - trustStoreFile = Paths.get("/x/y/truststore.jks"), - trustStoreType = "dummy" - ) + val options = ShellCmdLineOptions() + options.configFile = null + options.commandsDirectory = Paths.get("/x/y/commands") + options.cordappDirectory = Paths.get("/x/y/cordapps") + options.host = "alocalhost" + options.port = "1234" + options.user = "demo" + options.password = "abcd1234" + options.sshdPort = "2223" + options.sshdHostKeyDirectory = Paths.get("/x/y/ssh") + options.trustStorePassword = "pass2" + options.trustStoreFile = Paths.get("/x/y/truststore.jks") + options.trustStoreType = "dummy" val expectedSsl = ClientRpcSslOptions( trustStorePath = Paths.get("/x/y/truststore.jks"), @@ -114,21 +61,19 @@ class StandaloneShellArgsParserTest { @Test fun cmd_options_to_config_from_file() { - - val options = CommandLineOptions(configFile = CONFIG_FILE.toString(), - commandsDirectory = null, - cordappsDirectory = null, - host = null, - port = null, - user = null, - password = null, - help = false, - loggingLevel = Level.DEBUG, - sshdPort = null, - sshdHostKeyDirectory = null, - trustStorePassword = null, - trustStoreFile = null, - trustStoreType = null) + val options = ShellCmdLineOptions() + options.configFile = CONFIG_FILE + options.commandsDirectory = null + options.cordappDirectory = null + options.host = null + options.port = null + options.user = null + options.password = null + options.sshdPort = null + options.sshdHostKeyDirectory = null + options.trustStorePassword = null + options.trustStoreFile = null + options.trustStoreType = null val expectedConfig = ShellConfiguration( commandsDirectory = Paths.get("/x/y/commands"), @@ -148,21 +93,18 @@ class StandaloneShellArgsParserTest { @Test fun cmd_options_override_config_from_file() { - - val options = CommandLineOptions(configFile = CONFIG_FILE.toString(), - commandsDirectory = null, - cordappsDirectory = null, - host = null, - port = null, - user = null, - password = "blabla", - help = false, - loggingLevel = Level.DEBUG, - sshdPort = null, - sshdHostKeyDirectory = null, - trustStorePassword = null, - trustStoreFile = null, - trustStoreType = null) + val options = ShellCmdLineOptions() + options.configFile = CONFIG_FILE + options.commandsDirectory = null + options.host = null + options.port = null + options.user = null + options.password = "blabla" + options.sshdPort = null + options.sshdHostKeyDirectory = null + options.trustStorePassword = null + options.trustStoreFile = null + options.trustStoreType = null val expectedSsl = ClientRpcSslOptions( trustStorePath = Paths.get("/x/y/truststore.jks"),