[CORDA-1763]: Add node CLI option for validating configuration. (#4121)

This commit is contained in:
Michele Sollecito 2018-10-29 13:33:43 +00:00 committed by GitHub
parent 5086358834
commit 6022cecca5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 169 additions and 82 deletions

View File

@ -7,7 +7,6 @@ import java.util.Collections.emptySet
* It wraps either a valid [TARGET] or a set of [ERROR].
*/
interface Validated<TARGET, ERROR> {
/**
* The valid [TARGET] value.
*
@ -30,6 +29,11 @@ interface Validated<TARGET, ERROR> {
*/
val isInvalid: Boolean get() = !isValid
/**
* Returns the underlying value as optional, with a null result instead of an exception if validation rules were violated.
*/
val optional: TARGET? get() = if (isValid) value else null
/**
* Returns a valid [TARGET] if no validation errors are present. Otherwise, it throws the exception produced by [exceptionOnErrors], defaulting to [IllegalStateException].
*
@ -52,8 +56,29 @@ interface Validated<TARGET, ERROR> {
*/
fun <MAPPED_ERROR> mapErrors(convertError: (ERROR) -> MAPPED_ERROR): Validated<TARGET, MAPPED_ERROR>
companion object {
/**
* Performs the given [action] if the underlying value is valid.
* @return itself for fluent chained invocation.
*/
fun doIfValid(action: (TARGET) -> Unit): Validated<TARGET, ERROR> {
if (isValid) {
action.invoke(value)
}
return this
}
/**
* Performs the given [action] if the underlying value is invalid.
* @return itself for fluent chained invocation.
*/
fun doOnErrors(action: (Set<ERROR>) -> Unit): Validated<TARGET, ERROR> {
if (isInvalid) {
action.invoke(errors)
}
return this
}
companion object {
/**
* Constructs a [Validated] wrapper with given valid [target] value and no errors.
*/
@ -82,28 +107,23 @@ interface Validated<TARGET, ERROR> {
* Models the result of validating a [TARGET] value, producing [ERROR]s if rules are violated.
*/
sealed class Result<TARGET, ERROR> : Validated<TARGET, ERROR> {
/**
* A successful validation result, containing a valid [TARGET] value and no [ERROR]s.
*/
class Successful<TARGET, ERROR>(override val value: TARGET) : Result<TARGET, ERROR>(), Validated<TARGET, ERROR> {
override val errors: Set<ERROR> = emptySet<ERROR>()
override fun valueOrThrow(exceptionOnErrors: (Set<ERROR>) -> Exception) = value
override fun <MAPPED> map(convert: (TARGET) -> MAPPED): Validated<MAPPED, ERROR> {
return valid(convert.invoke(value))
}
override fun <MAPPED> mapValid(convert: (TARGET) -> Validated<MAPPED, ERROR>): Validated<MAPPED, ERROR> {
return convert.invoke(value)
}
override fun <MAPPED_ERROR> mapErrors(convertError: (ERROR) -> MAPPED_ERROR): Validated<TARGET, MAPPED_ERROR> {
return valid(value)
}
}
@ -112,7 +132,6 @@ interface Validated<TARGET, ERROR> {
* An unsuccessful validation result, containing [ERROR]s and no valid [TARGET] value.
*/
class Unsuccessful<TARGET, ERROR>(override val errors: Set<ERROR>) : Result<TARGET, ERROR>(), Validated<TARGET, ERROR> {
init {
require(errors.isNotEmpty())
}
@ -122,17 +141,14 @@ interface Validated<TARGET, ERROR> {
override fun valueOrThrow(exceptionOnErrors: (Set<ERROR>) -> Exception) = throw exceptionOnErrors.invoke(errors)
override fun <MAPPED> map(convert: (TARGET) -> MAPPED): Validated<MAPPED, ERROR> {
return invalid(errors)
}
override fun <MAPPED> mapValid(convert: (TARGET) -> Validated<MAPPED, ERROR>): Validated<MAPPED, ERROR> {
return invalid(errors)
}
override fun <MAPPED_ERROR> mapErrors(convertError: (ERROR) -> MAPPED_ERROR): Validated<TARGET, MAPPED_ERROR> {
return invalid(errors.asSequence().map(convertError).toSet())
}
}

View File

@ -1,20 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="info">
<Configuration status="debug">
<Properties>
<Property name="log-path">${sys:log-path:-logs}</Property>
<Property name="log-name">node-${hostName}</Property>
<Property name="archive">${log-path}/archive</Property>
<Property name="consoleLogLevel">${sys:consoleLogLevel:-error}</Property>
<Property name="defaultLogLevel">${sys:defaultLogLevel:-info}</Property>
<Property name="defaultLogLevel">${sys:log4j2.level:-info}</Property>
<Property name="consoleLogLevel">${sys:consoleLogLevel:-$defaultLogLevel}</Property>
<Property name="fileLogLevel">${sys:fileLogLevel:-$defaultLogLevel}</Property>
</Properties>
<ThresholdFilter level="trace"/>
<Appenders>
<Console name="Console-Appender" target="SYSTEM_OUT">
<PatternLayout>
<ScriptPatternSelector defaultPattern="%highlight{[%level{length=5}] %date{HH:mm:ssZ} [%t] %c{2}.%method - %msg%n %throwable{0}}{INFO=white,WARN=red,FATAL=bright red}">
<ScriptPatternSelector defaultPattern="%highlight{[%level{length=5}] %date{HH:mm:ssZ} [%t] %c{2}.%method - %msg%n%throwable{short.message}}{INFO=white,WARN=red,FATAL=bright red}">
<Script name="MDCSelector" language="javascript"><![CDATA[
result = null;
if (!logEvent.getContextData().size() == 0) {
@ -25,15 +24,14 @@
result;
]]>
</Script>
<PatternMatch key="WithMDC" pattern="%highlight{[%level{length=5}] %date{HH:mm:ssZ} [%t] %c{2}.%method - %msg %X%n}{INFO=white,WARN=red,FATAL=bright red}"/>
<PatternMatch key="WithMDC" pattern="%highlight{[%level{length=5}] %date{HH:mm:ssZ} [%t] %c{2}.%method - %msg %X%n%throwable{short.message}}{INFO=white,WARN=red,FATAL=bright red}"/>
</ScriptPatternSelector>
</PatternLayout>
<ThresholdFilter level="trace"/>
</Console>
<!-- Required for printBasicInfo -->
<Console name="Console-Appender-Println" target="SYSTEM_OUT">
<PatternLayout pattern="%msg%n%throwable{0}" />
<PatternLayout pattern="%msg%n%throwable{short.message}" />
</Console>
<!-- Will generate up to 100 log files for a given day. During every rollover it will delete
@ -42,7 +40,21 @@
fileName="${log-path}/${log-name}.log"
filePattern="${archive}/${log-name}.%date{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="[%-5level] %date{ISO8601}{UTC}Z [%t] %c{2}.%method - %msg %X%n"/>
<PatternLayout>
<ScriptPatternSelector defaultPattern="[%-5level] %date{ISO8601}{UTC}Z [%t] %c{2}.%method - %msg%n">
<Script name="MDCSelector" language="javascript"><![CDATA[
result = null;
if (!logEvent.getContextData().size() == 0) {
result = "WithMDC";
} else {
result = null;
}
result;
]]>
</Script>
<PatternMatch key="WithMDC" pattern="[%-5level] %date{ISO8601}{UTC}Z [%t] %c{2}.%method - %msg %X%n"/>
</ScriptPatternSelector>
</PatternLayout>
<Policies>
<TimeBasedTriggeringPolicy/>
@ -66,7 +78,7 @@
<Loggers>
<Root level="${defaultLogLevel}">
<AppenderRef ref="Console-Appender" level="${consoleLogLevel}"/>
<AppenderRef ref="RollingFile-Appender" />
<AppenderRef ref="RollingFile-Appender" level="${fileLogLevel}"/>
</Root>
<Logger name="BasicInfo" additivity="false">
<AppenderRef ref="Console-Appender-Println"/>

View File

@ -7,6 +7,8 @@ release, see :doc:`upgrade-notes`.
Unreleased
----------
* New "validate-configuration" sub-command to `corda.jar`, allowing to validate the actual node configuration without starting the node.
* Introduced new optional network bootstrapper command line option (--minimum-platform-version) to set as a network parameter
* Introduce minimum and target platform version for CorDapps.

View File

@ -77,6 +77,9 @@ Parameters:
``install-shell-extensions``: Install ``corda`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info.
``validate-configuration``: Validates the actual configuration without starting the node.
.. _enabling-remote-debugging:
Enabling remote debugging

View File

@ -72,6 +72,7 @@ dependencies {
compile project(':client:rpc')
compile project(':tools:shell')
compile project(':tools:cliutils')
compile project(':common-validation')
// Log4J: logging framework (with SLF4J bindings)
compile "org.apache.logging.log4j:log4j-slf4j-impl:${log4j_version}"

View File

@ -2,11 +2,9 @@ package net.corda.node
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigRenderOptions
import net.corda.core.internal.div
import net.corda.node.services.config.ConfigHelper
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.NodeConfigurationImpl
import net.corda.node.services.config.parseAsNodeConfiguration
import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy
import picocli.CommandLine.Option
@ -39,20 +37,9 @@ open class SharedNodeCmdLineOptions {
)
var devMode: Boolean? = null
open fun loadConfig(): NodeConfiguration {
return getRawConfig().parseAsNodeConfiguration(unknownConfigKeysPolicy::handle)
}
open fun parseConfiguration(configuration: Config): NodeConfiguration = configuration.parseAsNodeConfiguration(unknownConfigKeysPolicy::handle)
protected fun getRawConfig(): Config {
val rawConfig = ConfigHelper.loadConfig(
baseDirectory,
configFile
)
if (devMode == true) {
println("Config:\n${rawConfig.root().render(ConfigRenderOptions.defaults())}")
}
return rawConfig
}
open fun rawConfiguration(): Config = ConfigHelper.loadConfig(baseDirectory, configFile)
fun copyFrom(other: SharedNodeCmdLineOptions) {
baseDirectory = other.baseDirectory
@ -63,8 +50,8 @@ open class SharedNodeCmdLineOptions {
}
class InitialRegistrationCmdLineOptions : SharedNodeCmdLineOptions() {
override fun loadConfig(): NodeConfiguration {
return getRawConfig().parseAsNodeConfiguration(unknownConfigKeysPolicy::handle).also { config ->
override fun parseConfiguration(configuration: Config): NodeConfiguration {
return super.parseConfiguration(configuration).also { config ->
require(!config.devMode) { "Registration cannot occur in development mode" }
require(config.compatibilityZoneURL != null || config.networkServices != null) {
"compatibilityZoneURL or networkServices must be present in the node configuration file in registration mode."
@ -134,15 +121,8 @@ open class NodeCmdLineOptions : SharedNodeCmdLineOptions() {
)
var networkRootTrustStorePassword: String? = null
override fun loadConfig(): NodeConfiguration {
val rawConfig = ConfigHelper.loadConfig(
baseDirectory,
configFile,
configOverrides = ConfigFactory.parseMap(mapOf("noLocalShell" to this.noLocalShell) +
if (sshdServer) mapOf("sshd" to mapOf("port" to sshdServerPort.toString())) else emptyMap<String, Any>() +
if (devMode != null) mapOf("devMode" to this.devMode) else emptyMap())
)
return rawConfig.parseAsNodeConfiguration(unknownConfigKeysPolicy::handle).also { config ->
override fun parseConfiguration(configuration: Config): NodeConfiguration {
return super.parseConfiguration(configuration).also { config ->
if (isRegistration) {
require(!config.devMode) { "Registration cannot occur in development mode" }
require(config.compatibilityZoneURL != null || config.networkServices != null) {
@ -151,6 +131,18 @@ open class NodeCmdLineOptions : SharedNodeCmdLineOptions() {
}
}
}
override fun rawConfiguration(): Config {
val configOverrides = mutableMapOf<String, Any>()
configOverrides += "noLocalShell" to noLocalShell
if (sshdServer) {
configOverrides += "sshd" to mapOf("port" to sshdServerPort.toString())
}
devMode?.let {
configOverrides += "devMode" to it
}
return ConfigHelper.loadConfig(baseDirectory, configFile, configOverrides = ConfigFactory.parseMap(configOverrides))
}
}

View File

@ -1,6 +1,5 @@
package net.corda.node.internal
import com.typesafe.config.ConfigException
import io.netty.channel.unix.Errors
import net.corda.cliutils.*
import net.corda.core.crypto.Crypto
@ -15,12 +14,12 @@ import net.corda.node.*
import net.corda.node.internal.Node.Companion.isInvalidJavaVersion
import net.corda.node.internal.cordapp.MultipleCordappsForFlowException
import net.corda.node.internal.subcommands.*
import net.corda.node.internal.subcommands.ValidateConfigurationCli.Companion.logConfigurationErrors
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.shouldStartLocalShell
import net.corda.node.services.config.shouldStartSSHDaemon
import net.corda.node.utilities.registration.NodeRegistrationException
import net.corda.nodeapi.internal.addShutdownHook
import net.corda.nodeapi.internal.config.UnknownConfigurationKeysException
import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException
import net.corda.nodeapi.internal.persistence.DatabaseIncompatibleException
import net.corda.tools.shell.InteractiveShell
@ -57,15 +56,18 @@ abstract class NodeCliCommand(alias: String, description: String, val startup: N
/** Main corda entry point. */
open class NodeStartupCli : CordaCliWrapper("corda", "Runs a Corda Node") {
open val startup = NodeStartup()
@Mixin
val cmdLineOptions = NodeCmdLineOptions()
private val networkCacheCli by lazy { ClearNetworkCacheCli(startup) }
private val justGenerateNodeInfoCli by lazy { GenerateNodeInfoCli(startup) }
private val justGenerateRpcSslCertsCli by lazy { GenerateRpcSslCertsCli(startup) }
private val initialRegistrationCli by lazy { InitialRegistrationCli(startup) }
private val validateConfigurationCli by lazy { ValidateConfigurationCli() }
override fun initLogging() = this.initLogging(cmdLineOptions.baseDirectory)
override fun additionalSubCommands() = setOf(networkCacheCli, justGenerateNodeInfoCli, justGenerateRpcSslCertsCli, initialRegistrationCli)
override fun additionalSubCommands() = setOf(networkCacheCli, justGenerateNodeInfoCli, justGenerateRpcSslCertsCli, initialRegistrationCli, validateConfigurationCli)
override fun runProgram(): Int {
return when {
@ -104,9 +106,6 @@ open class NodeStartupCli : CordaCliWrapper("corda", "Runs a Corda Node") {
})
}
}
@Mixin
val cmdLineOptions = NodeCmdLineOptions()
}
/** This class provides a common set of functionality for starting a Node from command line arguments. */
@ -140,13 +139,7 @@ open class NodeStartup : NodeStartupLogging {
Node.printBasicNodeInfo(LOGS_CAN_BE_FOUND_IN_STRING, System.getProperty("log-path"))
// Step 5. Load and validate node configuration.
val configuration = (attempt { cmdLineOptions.loadConfig() }.doOnException(handleConfigurationLoadingError(cmdLineOptions.configFile)) as? Try.Success)?.let(Try.Success<NodeConfiguration>::value)
?: return ExitCodes.FAILURE
val errors = configuration.validate()
if (errors.isNotEmpty()) {
logger.error("Invalid node configuration. Errors were:${System.lineSeparator()}${errors.joinToString(System.lineSeparator())}")
return ExitCodes.FAILURE
}
val configuration = cmdLineOptions.nodeConfiguration().doOnErrors { errors -> logConfigurationErrors(errors, cmdLineOptions.configFile) }.optional ?: return ExitCodes.FAILURE
// Step 6. Configuring special serialisation requirements, i.e., bft-smart relies on Java serialization.
attempt { banJavaSerialisation(configuration) }.doOnException { error -> error.logAsUnexpected("Exception while configuring serialisation") } as? Try.Success
@ -430,23 +423,6 @@ interface NodeStartupLogging {
else -> error.logAsUnexpected("Exception during node startup")
}
}
fun handleConfigurationLoadingError(configFile: Path) = { error: Exception ->
when (error) {
is UnknownConfigurationKeysException -> error.logAsExpected()
is ConfigException.IO -> error.logAsExpected(configFileNotFoundMessage(configFile), ::println)
else -> error.logAsUnexpected("Unexpected error whilst reading node configuration")
}
}
private fun configFileNotFoundMessage(configFile: Path): String {
return """
Unable to load the node config file from '$configFile'.
Try setting the --base-directory flag to change which directory the node
is looking in, or use the --config-file flag to specify it explicitly.
""".trimIndent()
}
}
fun CliWrapperBase.initLogging(baseDirectory: Path) {

View File

@ -5,7 +5,7 @@ import net.corda.node.internal.NodeCliCommand
import net.corda.node.internal.NodeStartup
import net.corda.node.internal.RunAfterNodeInitialisation
class ClearNetworkCacheCli(startup: NodeStartup): NodeCliCommand("clear-network-cache", "Clears local copy of network map, on node startup it will be restored from server or file system.", startup) {
class ClearNetworkCacheCli(startup: NodeStartup): NodeCliCommand("clear-network-cache", "Clear local copy of network map, on node startup it will be restored from server or file system.", startup) {
override fun runProgram(): Int {
return startup.initialiseAndRun(cmdLineOptions, object: RunAfterNodeInitialisation {
override fun run(node: Node) = node.clearNetworkMapCache()

View File

@ -5,7 +5,7 @@ import net.corda.node.internal.NodeCliCommand
import net.corda.node.internal.NodeStartup
import net.corda.node.internal.RunAfterNodeInitialisation
class GenerateNodeInfoCli(startup: NodeStartup): NodeCliCommand("generate-node-info", "Performs the node start-up tasks necessary to generate the nodeInfo file, saves it to disk, then exits.", startup) {
class GenerateNodeInfoCli(startup: NodeStartup): NodeCliCommand("generate-node-info", "Perform the node start-up tasks necessary to generate the nodeInfo file, save it to disk, then exit.", startup) {
override fun runProgram(): Int {
return startup.initialiseAndRun(cmdLineOptions, object : RunAfterNodeInitialisation {
override fun run(node: Node) {

View File

@ -13,7 +13,7 @@ import net.corda.node.utilities.saveToTrustStore
import java.io.Console
import kotlin.system.exitProcess
class GenerateRpcSslCertsCli(startup: NodeStartup): NodeCliCommand("generate-rpc-ssl-settings", "Generates the SSL key and trust stores for a secure RPC connection.", startup) {
class GenerateRpcSslCertsCli(startup: NodeStartup): NodeCliCommand("generate-rpc-ssl-settings", "Generate the SSL key and trust stores for a secure RPC connection.", startup) {
override fun runProgram(): Int {
return startup.initialiseAndRun(cmdLineOptions, GenerateRpcSslCerts())
}

View File

@ -17,7 +17,7 @@ import picocli.CommandLine.Option
import java.io.File
import java.nio.file.Path
class InitialRegistrationCli(val startup: NodeStartup): CliWrapperBase("initial-registration", "Starts initial node registration with Corda network to obtain certificate from the permissioning server.") {
class InitialRegistrationCli(val startup: NodeStartup): CliWrapperBase("initial-registration", "Start initial node registration with Corda network to obtain certificate from the permissioning server.") {
@Option(names = ["-t", "--network-root-truststore"], description = ["Network root trust store obtained from network operator."])
var networkRootTrustStorePathParameter: Path? = null

View File

@ -0,0 +1,84 @@
package net.corda.node.internal.subcommands
import com.typesafe.config.Config
import com.typesafe.config.ConfigException
import com.typesafe.config.ConfigRenderOptions
import net.corda.cliutils.CliWrapperBase
import net.corda.cliutils.ExitCodes
import net.corda.common.validation.internal.Validated
import net.corda.common.validation.internal.Validated.Companion.invalid
import net.corda.common.validation.internal.Validated.Companion.valid
import net.corda.core.utilities.loggerFor
import net.corda.node.SharedNodeCmdLineOptions
import net.corda.node.internal.initLogging
import net.corda.node.services.config.NodeConfiguration
import picocli.CommandLine.*
import java.nio.file.Path
internal class ValidateConfigurationCli : CliWrapperBase("validate-configuration", "Validate the configuration without starting the node.") {
internal companion object {
private val logger = loggerFor<ValidateConfigurationCli>()
internal fun logConfigurationErrors(errors: Iterable<Exception>, configFile: Path) {
errors.forEach { error ->
when (error) {
is ConfigException.IO -> logger.error(configFileNotFoundMessage(configFile))
else -> logger.error("Error while parsing node configuration.", error)
}
}
}
private fun configFileNotFoundMessage(configFile: Path): String {
return """
Unable to load the node config file from '$configFile'.
Try setting the --base-directory flag to change which directory the node
is looking in, or use the --config-file flag to specify it explicitly.
""".trimIndent()
}
}
@Mixin
private val cmdLineOptions = SharedNodeCmdLineOptions()
override fun initLogging() = initLogging(cmdLineOptions.baseDirectory)
override fun runProgram(): Int {
val configuration = cmdLineOptions.nodeConfiguration()
if (configuration.isInvalid) {
logConfigurationErrors(configuration.errors, cmdLineOptions.configFile)
return ExitCodes.FAILURE
}
return ExitCodes.SUCCESS
}
}
internal fun SharedNodeCmdLineOptions.nodeConfiguration(): Valid<NodeConfiguration> = NodeConfigurationParser.invoke(this)
private object NodeConfigurationParser : (SharedNodeCmdLineOptions) -> Valid<NodeConfiguration> {
private val logger = loggerFor<ValidateConfigurationCli>()
private val configRenderingOptions = ConfigRenderOptions.defaults().setComments(false).setOriginComments(false).setFormatted(true)
override fun invoke(cmds: SharedNodeCmdLineOptions): Valid<NodeConfiguration> {
return attempt(cmds::rawConfiguration).doIfValid(::log).attemptMap(cmds::parseConfiguration).mapValid(::validate)
}
internal fun log(config: Config) = logger.debug("Actual configuration:\n${config.root().render(configRenderingOptions)}")
private fun validate(configuration: NodeConfiguration): Valid<NodeConfiguration> {
return Validated.withResult(configuration, configuration.validate().asSequence().map { error -> IllegalArgumentException(error) }.toSet())
}
private fun <VALUE, MAPPED> Valid<VALUE>.attemptMap(convert: (VALUE) -> MAPPED): Valid<MAPPED> = mapValid { value -> attempt { convert.invoke(value) } }
private fun <VALUE> attempt(action: () -> VALUE): Valid<VALUE> {
return try {
valid(action.invoke())
} catch (exception: Exception) {
return invalid(exception)
}
}
}
private typealias Valid<TARGET> = Validated<TARGET, Exception>

View File

@ -777,6 +777,7 @@ class DriverDSLImpl(
"visualvm.display.name" to "corda-${config.corda.myLegalName}"
)
debugPort?.let {
systemProperties += "log4j2.level" to "debug"
systemProperties += "log4j2.debug" to "true"
}