ENT-2611: Standardise CLI for corda firewall (#1503)

* Update firewall to use picocli

* Update documentation

* Remove joptsimple dependency

* Fix broken tests

* Grammar fix
This commit is contained in:
Anthony Keenan 2018-10-25 08:37:20 +01:00 committed by GitHub
parent 9edc15d018
commit abfe538e6e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 100 additions and 139 deletions

View File

@ -42,6 +42,10 @@ dependencies {
transitive = false// we control dependencies directly as the bridge is likely to be audited
}
compile(project(':tools:cliutils')) {
transitive = false// we control dependencies directly as the bridge is likely to be audited
}
// Here we pull in dependencies that would normally be pulled in transitively from :core and :node-api, but we need more fine grained control
// For AMQP serialisation.
compile "org.apache.qpid:proton-j:${protonj_version}"
@ -69,12 +73,15 @@ dependencies {
compile "org.slf4j:jul-to-slf4j:$slf4j_version"
compile "org.slf4j:jcl-over-slf4j:$slf4j_version"
// JOpt: for command line flags.
compile "net.sf.jopt-simple:jopt-simple:$jopt_simple_version"
// Manifests: for reading stuff from the manifest file
compile "com.jcabi:jcabi-manifests:1.1"
// Picocli: for parsing command line options
compile "info.picocli:picocli:$picocli_version"
// JAnsi: for drawing things to the terminal in nicely coloured ways.
compile "org.fusesource.jansi:jansi:$jansi_version"
integrationTestCompile project(':node-driver')
integrationTestCompile "org.apache.curator:curator-test:${curator_version}"
testCompile "junit:junit:$junit_version"

View File

@ -3,8 +3,9 @@
package net.corda.bridge
import net.corda.bridge.internal.FirewallStartup
import net.corda.cliutils.start
import kotlin.system.exitProcess
fun main(args: Array<String>) {
exitProcess(if (FirewallStartup(args).run()) 0 else 1)
FirewallStartup().start(args)
}

View File

@ -1,66 +0,0 @@
package net.corda.bridge
import joptsimple.OptionParser
import joptsimple.util.EnumConverter
import net.corda.bridge.services.api.FirewallConfiguration
import net.corda.bridge.services.config.BridgeConfigHelper
import net.corda.bridge.services.config.parseAsFirewallConfiguration
import net.corda.core.internal.div
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 ArgsParser {
private val optionParser = OptionParser()
// The intent of allowing a command line configurable directory and config path is to allow deployment flexibility.
// Other general configuration should live inside the config file unless we regularly need temporary overrides on the command line
private val baseDirectoryArg = optionParser
.accepts("base-directory", "The firewall working directory where all the files are kept")
.withRequiredArg()
.defaultsTo(".")
private val configFileArg = optionParser
.accepts("config-file", "The path to the config file")
.withRequiredArg()
.defaultsTo("firewall.conf")
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 logToConsoleArg = optionParser.accepts("log-to-console", "If set, prints logging to the console as well as to a file.")
private val isVersionArg = optionParser.accepts("version", "Print the version and exit")
private val helpArg = optionParser.accepts("help").forHelp()
fun parse(vararg args: String): CmdLineOptions {
val optionSet = optionParser.parse(*args)
val baseDirectory = Paths.get(optionSet.valueOf(baseDirectoryArg)).normalize().toAbsolutePath()
val configFilePath = Paths.get(optionSet.valueOf(configFileArg))
val configFile = if (configFilePath.isAbsolute) configFilePath else baseDirectory / configFilePath.toString()
val help = optionSet.has(helpArg)
val loggingLevel = optionSet.valueOf(loggerLevel)
val logToConsole = optionSet.has(logToConsoleArg)
val isVersion = optionSet.has(isVersionArg)
return CmdLineOptions(baseDirectory,
configFile,
help,
loggingLevel,
logToConsole,
isVersion)
}
fun printHelp(sink: PrintStream) = optionParser.printHelpOn(sink)
}
data class CmdLineOptions(val baseDirectory: Path,
val configFile: Path,
val help: Boolean,
val loggingLevel: Level,
val logToConsole: Boolean,
val isVersion: Boolean) {
fun loadConfig(): FirewallConfiguration {
val config = BridgeConfigHelper.loadConfig(baseDirectory, configFile).parseAsFirewallConfiguration()
return config
}
}

View File

@ -0,0 +1,28 @@
package net.corda.bridge
import net.corda.bridge.services.api.FirewallConfiguration
import net.corda.bridge.services.config.BridgeConfigHelper
import net.corda.bridge.services.config.parseAsFirewallConfiguration
import net.corda.core.internal.div
import picocli.CommandLine.Option
import java.nio.file.Path
import java.nio.file.Paths
class FirewallCmdLineOptions {
@Option(
names = ["-b", "--base-directory"],
description = ["The firewall working directory where all the files are kept."]
)
var baseDirectory: Path = Paths.get(".").toAbsolutePath().normalize()
@Option(
names = ["-f", "--config-file"],
description = ["The path to the config file. By default this is firewall.conf in the base directory."]
)
private var _configFile: Path? = null
val configFile: Path get() = _configFile ?: (baseDirectory / "firewall.conf")
fun loadConfig(): FirewallConfiguration {
return BridgeConfigHelper.loadConfig(baseDirectory, configFile).parseAsFirewallConfiguration()
}
}

View File

@ -1,74 +1,64 @@
package net.corda.bridge.internal
import com.jcabi.manifests.Manifests
import joptsimple.OptionException
import net.corda.bridge.ArgsParser
import net.corda.bridge.CmdLineOptions
import net.corda.bridge.FirewallCmdLineOptions
import net.corda.bridge.FirewallVersionInfo
import net.corda.bridge.services.api.FirewallConfiguration
import net.corda.cliutils.CordaCliWrapper
import net.corda.cliutils.CordaVersionProvider
import net.corda.cliutils.ExitCodes
import net.corda.core.internal.createDirectories
import net.corda.core.internal.div
import net.corda.core.utilities.contextLogger
import net.corda.nodeapi.internal.addShutdownHook
import org.slf4j.bridge.SLF4JBridgeHandler
import picocli.CommandLine.Mixin
import sun.misc.VMSupport
import java.io.RandomAccessFile
import java.lang.management.ManagementFactory
import java.net.InetAddress
import java.nio.file.Path
import java.util.*
import kotlin.system.exitProcess
class FirewallStartup(val args: Array<String>) {
class FirewallStartup: CordaCliWrapper("corda-firewall", "The Corda Firewall application for handling outbound and inbound connections to Corda.") {
companion object {
// lazy init the logging, because the logging levels aren't configured until we have parsed some options.
private val log by lazy { contextLogger() }
val LOGS_DIRECTORY_NAME = "logs"
}
@Mixin
val cmdLineOptions = FirewallCmdLineOptions()
/**
* @return true if the firewalls startup was successful. This value is intended to be the exit code of the process.
* @return zero if the firewalls startup was successful. This value is the exit code of the process.
*/
fun run(): Boolean {
override fun runProgram(): Int {
val startTime = System.currentTimeMillis()
val (argsParser, cmdlineOptions) = parseArguments()
// We do the single firewall check before we initialise logging so that in case of a double-firewall start it
// doesn't mess with the running firewall's logs.
enforceSingleBridgeIsRunning(cmdlineOptions.baseDirectory)
enforceSingleBridgeIsRunning(cmdLineOptions.baseDirectory)
initLogging(cmdlineOptions)
initLogging()
val versionInfo = getVersionInfo()
if (cmdlineOptions.isVersion) {
println("${versionInfo.vendor} ${versionInfo.releaseVersion}")
println("Revision ${versionInfo.revision}")
println("Platform Version ${versionInfo.platformVersion}")
return true
}
// Maybe render command line help.
if (cmdlineOptions.help) {
argsParser.printHelp(System.out)
return true
}
val conf = try {
loadConfigFile(cmdlineOptions)
loadConfigFile()
} catch (e: Exception) {
log.error("Exception during firewall configuration", e)
return false
return ExitCodes.FAILURE
}
try {
logStartupInfo(versionInfo, cmdlineOptions, conf)
logStartupInfo(versionInfo, conf)
} catch (e: Exception) {
log.error("Exception during firewall registration", e)
return false
return ExitCodes.FAILURE
}
val firewall = try {
cmdlineOptions.baseDirectory.createDirectories()
cmdLineOptions.baseDirectory.createDirectories()
startFirewall(conf, versionInfo, startTime)
} catch (e: Exception) {
if (e.message?.startsWith("Unknown named curve:") == true) {
@ -77,7 +67,7 @@ class FirewallStartup(val args: Array<String>) {
} else {
log.error("Exception during firewall startup", e)
}
return false
return ExitCodes.FAILURE
}
if (System.getProperties().containsKey("WAIT_KEY_FOR_EXIT")) {
@ -89,10 +79,10 @@ class FirewallStartup(val args: Array<String>) {
log.info("firewall shutting down")
firewall.stop()
return true
return ExitCodes.SUCCESS
}
fun logStartupInfo(versionInfo: FirewallVersionInfo, cmdlineOptions: CmdLineOptions, conf: FirewallConfiguration) {
fun logStartupInfo(versionInfo: FirewallVersionInfo, conf: FirewallConfiguration) {
log.info("Vendor: ${versionInfo.vendor}")
log.info("Release: ${versionInfo.releaseVersion}")
log.info("Platform Version: ${versionInfo.platformVersion}")
@ -106,7 +96,7 @@ class FirewallStartup(val args: Array<String>) {
log.info("classpath: ${info.classPath}")
log.info("VM ${info.vmName} ${info.vmVendor} ${info.vmVersion}")
log.info("Machine: ${lookupMachineNameAndMaybeWarn()}")
log.info("Working Directory: ${cmdlineOptions.baseDirectory}")
log.info("Working Directory: ${cmdLineOptions.baseDirectory}")
val agentProperties = VMSupport.getAgentProperties()
if (agentProperties.containsKey("sun.jdwp.listenerAddress")) {
log.info("Debug port: ${agentProperties.getProperty("sun.jdwp.listenerAddress")}")
@ -114,20 +104,16 @@ class FirewallStartup(val args: Array<String>) {
log.info("Starting as firewall mode of ${conf.firewallMode}")
}
protected fun loadConfigFile(cmdlineOptions: CmdLineOptions): FirewallConfiguration = cmdlineOptions.loadConfig()
protected fun loadConfigFile(): FirewallConfiguration = cmdLineOptions.loadConfig()
protected fun getVersionInfo(): FirewallVersionInfo {
// Manifest properties are only available if running from the corda jar
fun manifestValue(name: String): String? = if (Manifests.exists(name)) Manifests.read(name) else null
return FirewallVersionInfo(
manifestValue("Corda-Platform-Version")?.toInt() ?: 1,
manifestValue("Corda-Release-Version") ?: "Unknown",
manifestValue("Corda-Revision") ?: "Unknown",
manifestValue("Corda-Vendor") ?: "Unknown"
CordaVersionProvider.platformVersion,
CordaVersionProvider.releaseVersion,
CordaVersionProvider.revision,
CordaVersionProvider.vendor
)
}
private fun enforceSingleBridgeIsRunning(baseDirectory: Path) {
// Write out our process ID (which may or may not resemble a UNIX process id - to us it's just a string) to a
// file that we'll do our best to delete on exit. But if we don't, it'll be overwritten next time. If it already
@ -171,25 +157,13 @@ class FirewallStartup(val args: Array<String>) {
return hostName
}
private fun parseArguments(): Pair<ArgsParser, CmdLineOptions> {
val argsParser = ArgsParser()
val cmdlineOptions = try {
argsParser.parse(*args)
} catch (ex: OptionException) {
println("Invalid command line arguments: ${ex.message}")
argsParser.printHelp(System.out)
exitProcess(1)
}
return Pair(argsParser, cmdlineOptions)
}
fun initLogging(cmdlineOptions: CmdLineOptions) {
val loggingLevel = cmdlineOptions.loggingLevel.name.toLowerCase(Locale.ENGLISH)
override fun initLogging() {
val loggingLevel = loggingLevel.name.toLowerCase(Locale.ENGLISH)
System.setProperty("defaultLogLevel", loggingLevel) // These properties are referenced from the XML config file.
if (cmdlineOptions.logToConsole) {
if (verbose) {
System.setProperty("consoleLogLevel", loggingLevel)
}
System.setProperty("log-path", (cmdlineOptions.baseDirectory / LOGS_DIRECTORY_NAME).toString())
System.setProperty("log-path", (cmdLineOptions.baseDirectory / LOGS_DIRECTORY_NAME).toString())
SLF4JBridgeHandler.removeHandlersForRootLogger() // The default j.u.l config adds a ConsoleHandler.
SLF4JBridgeHandler.install()
}

View File

@ -42,15 +42,14 @@ fun createNetworkParams(baseDirectory: Path): Int {
fun createAndLoadConfigFromResource(baseDirectory: Path, configResource: String): FirewallConfiguration {
val workspaceFolder = baseDirectory.normalize().toAbsolutePath()
val args = arrayOf("--base-directory", workspaceFolder.toString())
val argsParser = ArgsParser()
val cmdlineOptions = argsParser.parse(*args)
val configFile = cmdlineOptions.configFile
val cmdLineOptions = FirewallCmdLineOptions()
cmdLineOptions.baseDirectory = workspaceFolder
val configFile = cmdLineOptions.configFile
configFile.normalize().parent?.createDirectories()
ConfigTest::class.java.getResourceAsStream(configResource).use {
Files.copy(it, configFile)
}
val config = cmdlineOptions.loadConfig()
val config = cmdLineOptions.loadConfig()
return config
}

View File

@ -74,3 +74,11 @@ List of existing CLI applications
| :doc:`Blob inspector<blob-inspector>` | ``corda-tools-blob-inspector-<version>.jar`` | ``blob-inspector --<option>`` |
+----------------------------------------------------------------+--------------------------------------------------------------+--------------------------------+
List of existing Enterprise CLI applications
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+----------------------------------------------------------------+--------------------------------------------------------------+--------------------------------+
| Description | JAR name | Alias |
+----------------------------------------------------------------+--------------------------------------------------------------+--------------------------------+
| :ref:`Corda Firewall<firewall-coniguration-file>` | ``corda-firewall-<version>.jar`` | ``corda-firewall --<option>`` |
+----------------------------------------------------------------+--------------------------------------------------------------+--------------------------------+

View File

@ -6,16 +6,26 @@ Firewall configuration
File location
-------------
When starting a standalone firewall (in bridge, or float mode), the ``corda-firewall.jar`` file defaults to reading the firewall's configuration from a ``firewall.conf`` file in
the directory from which the command to launch the process is executed. There are two command-line options to override this
behaviour:
the directory from which the command to launch the process is executed. The syntax is:
* The ``--config-file`` command line option allows you to specify a configuration file with a different name, or at
different file location. Paths are relative to the current working directory
* The ``--base-directory`` command line option allows you to specify the firewall's workspace location. A ``firewall.conf``
.. code:: bash
corda-firewall [-hvV] [--install-shell-extensions]
[--logging-level=<loggingLevel>] [-b=<baseDirectory>]
[-f=<_configFile>]
Where:
* ``--config-file``, ``-f``: Allows you to specify a configuration file with a different name, or at
a different file location. Paths are relative to the current working directory
* ``--base-directory``, ``-b``: Allows you to specify the firewall's workspace location. A ``firewall.conf``
configuration file is then expected in the root of this workspace
If you specify both command line arguments at the same time, the firewall will fail to start.
* ``--verbose``, ``--log-to-console``, ``-v``: If set, prints logging to the console as well as to a file.
* ``--logging-level=<loggingLevel>``: Enable logging at this level and higher. Possible values: ERROR, WARN, INFO, DEBUG, TRACE. Default: INFO.
* ``--install-shell-extensions``: Install ``corda-firewall`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info.
* ``--help``, ``-h``: Show this help message and exit.
* ``--version``, ``-V``: Print version information and exit.
Format
------