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 f4efafcc9d.

* 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
This commit is contained in:
Anthony Keenan 2018-09-13 11:14:31 +01:00 committed by GitHub
parent ec0fc849ed
commit 046b104fee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 297 additions and 378 deletions

View File

@ -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

View File

@ -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'

View File

@ -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)

View File

@ -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

View File

@ -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'
)
}

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="info">
<Properties>
<Property name="logLevel">off</Property>
</Properties>
<Appenders>
<Console name="STDOUT" target="SYSTEM_OUT" ignoreExceptions="false">
<PatternLayout pattern="[%C{1}.%M] %m%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="${sys:logLevel}">
<AppenderRef ref="STDOUT"/>
</Root>
</Loggers>
</Configuration>

View File

@ -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"

View File

@ -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<Level> {
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")
}

View File

@ -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:

View File

@ -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'

View File

@ -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<String, Any?>()
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<ShellConfigurationFile.ShellConfigFile>()
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 })
}
}
}

View File

@ -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<String>) {
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<URL> =
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
}
}

View File

@ -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>(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<String, Any?>()
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<ShellConfigurationFile.ShellConfigFile>()
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 })
}
}
}

View File

@ -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<String>()
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"),