Another approach to fixing deployNodes task and network parameters generation (#2066)

* Generate networkParameteres for Cordformation.

Fix deployNodes task in Cordformation to generate NetworkParameters before running the nodes.
Add TestNetworkParametersGenerator utility loaded after node infos generation step.

* Get rid of bouncy castle provider dependency
For cordform-common. It caused problems with loading our custom
X509EdDSAEngine for generation of network parameters in deployNodes
task.
This commit is contained in:
Katarzyna Streich 2017-11-30 10:39:29 +00:00 committed by GitHub
parent 572c4af40c
commit c9f3e98795
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 185 additions and 64 deletions

View File

@ -1,4 +1,4 @@
gradlePluginsVersion=2.0.9 gradlePluginsVersion=3.0.0-NETWORKMAP
kotlinVersion=1.1.60 kotlinVersion=1.1.60
guavaVersion=21.0 guavaVersion=21.0
bouncycastleVersion=1.57 bouncycastleVersion=1.57

View File

@ -13,9 +13,6 @@ group 'net.corda.plugins'
dependencies { dependencies {
// TypeSafe Config: for simple and human friendly config files. // TypeSafe Config: for simple and human friendly config files.
compile "com.typesafe:config:$typesafe_config_version" compile "com.typesafe:config:$typesafe_config_version"
// Bouncy Castle: for X.500 distinguished name manipulation
compile "org.bouncycastle:bcprov-jdk15on:$bouncycastle_version"
} }
publish { publish {

View File

@ -1,6 +1,5 @@
package net.corda.cordform; package net.corda.cordform;
import org.bouncycastle.asn1.x500.X500Name;
import java.nio.file.Path; import java.nio.file.Path;
public interface CordformContext { public interface CordformContext {

View File

@ -0,0 +1,16 @@
package net.corda.cordform;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
public interface NetworkParametersGenerator {
/**
* Run generation of network parameters for [Cordformation]. Nodes need to have already their own [NodeInfo] files in their
* base directories, these files will be used to extract notary identities.
*
* @param nodesDirs - nodes directories that will be used for network parameters generation. Network parameters
* file will be dropped into each directory on this list.
*/
void run(List<Path> nodesDirs);
}

View File

@ -3,15 +3,14 @@ package net.corda.plugins
import groovy.lang.Closure import groovy.lang.Closure
import net.corda.cordform.CordformDefinition import net.corda.cordform.CordformDefinition
import net.corda.cordform.CordformNode import net.corda.cordform.CordformNode
import net.corda.cordform.NetworkParametersGenerator
import org.apache.tools.ant.filters.FixCrLfFilter import org.apache.tools.ant.filters.FixCrLfFilter
import org.gradle.api.DefaultTask import org.gradle.api.DefaultTask
import org.gradle.api.GradleException import org.gradle.api.GradleException
import org.gradle.api.plugins.JavaPluginConvention import org.gradle.api.plugins.JavaPluginConvention
import org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME import org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME
import org.gradle.api.tasks.TaskAction import org.gradle.api.tasks.TaskAction
import java.io.File
import java.net.URLClassLoader import java.net.URLClassLoader
import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.Paths import java.nio.file.Paths
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -28,6 +27,7 @@ open class Cordform : DefaultTask() {
*/ */
@Suppress("MemberVisibilityCanPrivate") @Suppress("MemberVisibilityCanPrivate")
var definitionClass: String? = null var definitionClass: String? = null
private val networkParametersGenClass: String = "net.corda.nodeapi.internal.TestNetworkParametersGenerator"
private var directory = Paths.get("build", "nodes") private var directory = Paths.get("build", "nodes")
private val nodes = mutableListOf<Node>() private val nodes = mutableListOf<Node>()
@ -113,6 +113,19 @@ open class Cordform : DefaultTask() {
.newInstance() .newInstance()
} }
/**
* The parametersGenerator needn't be compiled until just before our build method, so we load it manually via sourceSets.main.runtimeClasspath.
*/
private fun loadParametersGenerator(): NetworkParametersGenerator {
val plugin = project.convention.getPlugin(JavaPluginConvention::class.java)
val classpath = plugin.sourceSets.getByName(MAIN_SOURCE_SET_NAME).runtimeClasspath
val urls = classpath.files.map { it.toURI().toURL() }.toTypedArray()
return URLClassLoader(urls, NetworkParametersGenerator::class.java.classLoader)
.loadClass(networkParametersGenClass)
.asSubclass(NetworkParametersGenerator::class.java)
.newInstance()
}
/** /**
* This task action will create and install the nodes based on the node configurations added. * This task action will create and install the nodes based on the node configurations added.
*/ */
@ -124,6 +137,7 @@ open class Cordform : DefaultTask() {
installRunScript() installRunScript()
nodes.forEach(Node::build) nodes.forEach(Node::build)
generateAndInstallNodeInfos() generateAndInstallNodeInfos()
generateAndInstallNetworkParameters()
} }
private fun initializeConfiguration() { private fun initializeConfiguration() {
@ -142,6 +156,12 @@ open class Cordform : DefaultTask() {
} }
} }
private fun generateAndInstallNetworkParameters() {
project.logger.info("Generating and installing network parameters")
val networkParamsGenerator = loadParametersGenerator()
networkParamsGenerator.run(nodes.map { it.fullPath() })
}
private fun generateAndInstallNodeInfos() { private fun generateAndInstallNodeInfos() {
generateNodeInfos() generateNodeInfos()
installNodeInfos() installNodeInfos()
@ -149,7 +169,7 @@ open class Cordform : DefaultTask() {
private fun generateNodeInfos() { private fun generateNodeInfos() {
project.logger.info("Generating node infos") project.logger.info("Generating node infos")
var nodeProcesses = buildNodeProcesses() val nodeProcesses = buildNodeProcesses()
try { try {
validateNodeProcessess(nodeProcesses) validateNodeProcessess(nodeProcesses)
} finally { } finally {
@ -158,9 +178,10 @@ open class Cordform : DefaultTask() {
} }
private fun buildNodeProcesses(): Map<Node, Process> { private fun buildNodeProcesses(): Map<Node, Process> {
return nodes val command = generateNodeInfoCommand()
.map { buildNodeProcess(it) } return nodes.map {
.toMap() it.makeLogDirectory()
buildProcess(it, command, "generate-info.log") }.toMap()
} }
private fun validateNodeProcessess(nodeProcesses: Map<Node, Process>) { private fun validateNodeProcessess(nodeProcesses: Map<Node, Process>) {
@ -175,14 +196,13 @@ open class Cordform : DefaultTask() {
} }
} }
private fun buildNodeProcess(node: Node): Pair<Node, Process> { private fun buildProcess(node: Node, command: List<String>, logFile: String): Pair<Node, Process> {
node.makeLogDirectory() val process = ProcessBuilder(command)
var process = ProcessBuilder(generateNodeInfoCommand())
.directory(node.fullPath().toFile()) .directory(node.fullPath().toFile())
.redirectErrorStream(true) .redirectErrorStream(true)
// InheritIO causes hangs on windows due the gradle buffer also not being flushed. // InheritIO causes hangs on windows due the gradle buffer also not being flushed.
// Must redirect to output or logger (node log is still written, this is just startup banner) // Must redirect to output or logger (node log is still written, this is just startup banner)
.redirectOutput(node.logFile().toFile()) .redirectOutput(node.logFile(logFile).toFile())
.addEnvironment("CAPSULE_CACHE_DIR", Node.capsuleCacheDir) .addEnvironment("CAPSULE_CACHE_DIR", Node.capsuleCacheDir)
.start() .start()
return Pair(node, process) return Pair(node, process)
@ -224,6 +244,6 @@ open class Cordform : DefaultTask() {
} }
} }
} }
private fun Node.logFile(): Path = this.logDirectory().resolve("generate-info.log") private fun Node.logFile(name: String): Path = this.logDirectory().resolve(name)
private fun ProcessBuilder.addEnvironment(key: String, value: String) = this.apply { environment().put(key, value) } private fun ProcessBuilder.addEnvironment(key: String, value: String) = this.apply { environment().put(key, value) }
} }

View File

@ -2,9 +2,6 @@ package net.corda.plugins
import com.typesafe.config.* import com.typesafe.config.*
import net.corda.cordform.CordformNode import net.corda.cordform.CordformNode
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x500.RDN
import org.bouncycastle.asn1.x500.style.BCStyle
import org.gradle.api.Project import org.gradle.api.Project
import java.io.File import java.io.File
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
@ -122,18 +119,10 @@ class Node(private val project: Project) : CordformNode() {
project.logger.error("Node has a null name - cannot create node") project.logger.error("Node has a null name - cannot create node")
throw IllegalStateException("Node has a null name - cannot create node") throw IllegalStateException("Node has a null name - cannot create node")
} }
// Parsing O= part directly because importing BouncyCastle provider in Cordformation causes problems
val dirName = try { // with loading our custom X509EdDSAEngine.
val o = X500Name(name).getRDNs(BCStyle.O) val organizationName = name.trim().split(",").firstOrNull { it.startsWith("O=") }?.substringAfter("=")
if (o.size > 0) { val dirName = organizationName ?: name
o.first().first.value.toString()
} else {
name
}
} catch(_ : IllegalArgumentException) {
// Can't parse as an X500 name, use the full string
name
}
nodeDir = File(rootDir.toFile(), dirName) nodeDir = File(rootDir.toFile(), dirName)
} }
@ -151,7 +140,7 @@ class Node(private val project: Project) : CordformNode() {
* Installs the corda fat JAR to the node directory. * Installs the corda fat JAR to the node directory.
*/ */
private fun installCordaJar() { private fun installCordaJar() {
val cordaJar = verifyAndGetCordaJar() val cordaJar = verifyAndGetRuntimeJar("corda")
project.copy { project.copy {
it.apply { it.apply {
from(cordaJar) from(cordaJar)
@ -166,7 +155,7 @@ class Node(private val project: Project) : CordformNode() {
* Installs the corda webserver JAR to the node directory * Installs the corda webserver JAR to the node directory
*/ */
private fun installWebserverJar() { private fun installWebserverJar() {
val webJar = verifyAndGetWebserverJar() val webJar = verifyAndGetRuntimeJar("corda-webserver")
project.copy { project.copy {
it.apply { it.apply {
from(webJar) from(webJar)
@ -250,34 +239,17 @@ class Node(private val project: Project) : CordformNode() {
} }
/** /**
* Find the corda JAR amongst the dependencies. * Find the given JAR amongst the dependencies
* @param jarName JAR name without the version part, for example for corda-2.0-SNAPSHOT.jar provide only "corda" as jarName
* *
* @return A file representing the Corda JAR. * @return A file representing found JAR
*/ */
private fun verifyAndGetCordaJar(): File { private fun verifyAndGetRuntimeJar(jarName: String): File {
val maybeCordaJAR = project.configuration("runtime").filter {
it.toString().contains("corda-$releaseVersion.jar") || it.toString().contains("corda-enterprise-$releaseVersion.jar")
}
if (maybeCordaJAR.isEmpty) {
throw RuntimeException("No Corda Capsule JAR found. Have you deployed the Corda project to Maven? Looked for \"corda-$releaseVersion.jar\"")
} else {
val cordaJar = maybeCordaJAR.singleFile
assert(cordaJar.isFile)
return cordaJar
}
}
/**
* Find the corda JAR amongst the dependencies
*
* @return A file representing the Corda webserver JAR
*/
private fun verifyAndGetWebserverJar(): File {
val maybeJar = project.configuration("runtime").filter { val maybeJar = project.configuration("runtime").filter {
it.toString().contains("corda-webserver-$releaseVersion.jar") "$jarName-$releaseVersion.jar" in it.toString() || "$jarName-enterprise-$releaseVersion.jar" in it.toString()
} }
if (maybeJar.isEmpty) { if (maybeJar.isEmpty) {
throw RuntimeException("No Corda Webserver JAR found. Have you deployed the Corda project to Maven? Looked for \"corda-webserver-$releaseVersion.jar\"") throw IllegalStateException("No $jarName JAR found. Have you deployed the Corda project to Maven? Looked for \"$jarName-$releaseVersion.jar\"")
} else { } else {
val jar = maybeJar.singleFile val jar = maybeJar.singleFile
assert(jar.isFile) assert(jar.isFile)

View File

@ -1,4 +1,4 @@
package net.corda.testing.common.internal package net.corda.nodeapi.internal
import net.corda.core.crypto.SignedData import net.corda.core.crypto.SignedData
import net.corda.core.crypto.entropyToKeyPair import net.corda.core.crypto.entropyToKeyPair

View File

@ -0,0 +1,115 @@
package net.corda.nodeapi.internal
import com.typesafe.config.ConfigFactory
import net.corda.cordform.CordformNode
import net.corda.cordform.NetworkParametersGenerator
import net.corda.core.crypto.SignedData
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.div
import net.corda.core.internal.list
import net.corda.core.internal.readAll
import net.corda.core.node.NodeInfo
import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.internal.SerializationEnvironmentImpl
import net.corda.core.serialization.internal._contextSerializationEnv
import net.corda.core.utilities.ByteSequence
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.days
import net.corda.nodeapi.internal.serialization.*
import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme
import net.corda.nodeapi.internal.serialization.kryo.AbstractKryoSerializationScheme
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
import java.nio.file.Path
import java.time.Instant
import kotlin.streams.toList
// This class is used by deployNodes task to generate NetworkParameters in [Cordformation].
@Suppress("UNUSED")
class TestNetworkParametersGenerator : NetworkParametersGenerator {
companion object {
private val logger = contextLogger()
}
override fun run(nodesDirs: List<Path>) {
logger.info("NetworkParameters generation using node directories: $nodesDirs")
try {
initialiseSerialization()
val notaryInfos = loadAndGatherNotaryIdentities(nodesDirs)
val copier = NetworkParametersCopier(NetworkParameters(
minimumPlatformVersion = 1,
notaries = notaryInfos,
modifiedTime = Instant.now(),
eventHorizon = 10000.days,
maxMessageSize = 40000,
maxTransactionSize = 40000,
epoch = 1
))
nodesDirs.forEach { copier.install(it) }
} finally {
_contextSerializationEnv.set(null)
}
}
private fun loadAndGatherNotaryIdentities(nodesDirs: List<Path>): List<NotaryInfo> {
val infos = getAllNodeInfos(nodesDirs)
val configs = nodesDirs.map { ConfigFactory.parseFile((it / "node.conf").toFile()) }
val notaryConfigs = configs.filter { it.hasPath("notary") }
val notaries = notaryConfigs.associateBy(
{ CordaX500Name.parse(it.getString("myLegalName")) },
{ it.getConfig("notary").getBoolean("validating") }
)
// Now get the notary identities based on names passed from configs. There is one problem, for distributed notaries
// in config we specify only node's main name, the notary identity isn't passed there. It's read from keystore on
// node startup, so we have to look it up from node info as a second identity, which is ugly.
return infos.mapNotNull {
info -> notaries[info.legalIdentities[0].name]?.let { NotaryInfo(info.notaryIdentity(), it) }
}.distinct()
}
/**
* Loads latest NodeInfo files stored in node's base directory.
* Scans main directory and [CordformNode.NODE_INFO_DIRECTORY].
* Signatures are checked before returning a value. The latest value stored for a given name is returned.
*
* @return list of latest [NodeInfo]s
*/
private fun getAllNodeInfos(nodesDirs: List<Path>): List<NodeInfo> {
val nodeInfoFiles = nodesDirs.map { dir -> dir.list { it.filter { "nodeInfo-" in it.toString() }.toList()[0] } } // We take the first one only
return nodeInfoFiles.mapNotNull { processFile(it) }
}
private fun processFile(file: Path): NodeInfo? {
return try {
logger.info("Reading NodeInfo from file: $file")
val signedData = file.readAll().deserialize<SignedData<NodeInfo>>()
signedData.verified()
} catch (e: Exception) {
logger.warn("Exception parsing NodeInfo from file. $file", e)
null
}
}
private fun NodeInfo.notaryIdentity() = if (legalIdentities.size == 2) legalIdentities[1] else legalIdentities[0]
// We need to to set serialization env, because generation of parameters is run from Cordform.
// KryoServerSerializationScheme is not accessible from nodeapi.
private fun initialiseSerialization() {
val context = if (java.lang.Boolean.getBoolean("net.corda.testing.amqp.enable")) AMQP_P2P_CONTEXT else KRYO_P2P_CONTEXT
_contextSerializationEnv.set(SerializationEnvironmentImpl(
SerializationFactoryImpl().apply {
registerScheme(KryoParametersSerializationScheme)
registerScheme(AMQPServerSerializationScheme())
},
context))
}
private object KryoParametersSerializationScheme : AbstractKryoSerializationScheme() {
override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean {
return byteSequence == KryoHeaderV0_1 && target == SerializationContext.UseCase.P2P
}
override fun rpcClientKryoPool(context: SerializationContext) = throw UnsupportedOperationException()
override fun rpcServerKryoPool(context: SerializationContext) = throw UnsupportedOperationException()
}
}

View File

@ -27,7 +27,7 @@ import net.corda.node.services.transactions.minCorrectReplicas
import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.node.utilities.ServiceIdentityGenerator
import net.corda.nodeapi.internal.NotaryInfo import net.corda.nodeapi.internal.NotaryInfo
import net.corda.testing.chooseIdentity import net.corda.testing.chooseIdentity
import net.corda.testing.common.internal.NetworkParametersCopier import net.corda.nodeapi.internal.NetworkParametersCopier
import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyContract
import net.corda.testing.dummyCommand import net.corda.testing.dummyCommand

View File

@ -63,6 +63,7 @@ import org.apache.activemq.artemis.utils.ReusableLatch
import org.slf4j.Logger import org.slf4j.Logger
import rx.Observable import rx.Observable
import java.io.IOException import java.io.IOException
import java.io.NotSerializableException
import java.lang.reflect.InvocationTargetException import java.lang.reflect.InvocationTargetException
import java.security.KeyPair import java.security.KeyPair
import java.security.KeyStoreException import java.security.KeyStoreException

View File

@ -27,6 +27,7 @@ class RPCMessagingClient(private val config: SSLConfiguration, serverAddress: Ne
} }
fun stop() = synchronized(this) { fun stop() = synchronized(this) {
rpcServer?.close()
artemis.stop() artemis.stop()
} }
} }

View File

@ -38,7 +38,7 @@ dependencies {
// Specify your cordapp's dependencies below, including dependent cordapps // Specify your cordapp's dependencies below, including dependent cordapps
compile group: 'commons-io', name: 'commons-io', version: '2.5' compile group: 'commons-io', name: 'commons-io', version: '2.5'
testCompile project(':node-driver') cordaCompile project(':node-driver')
testCompile "junit:junit:$junit_version" testCompile "junit:junit:$junit_version"
testCompile "org.assertj:assertj-core:${assertj_version}" testCompile "org.assertj:assertj-core:${assertj_version}"
} }

View File

@ -40,7 +40,7 @@ fun <A> springDriver(
useTestClock: Boolean = defaultParameters.useTestClock, useTestClock: Boolean = defaultParameters.useTestClock,
initialiseSerialization: Boolean = defaultParameters.initialiseSerialization, initialiseSerialization: Boolean = defaultParameters.initialiseSerialization,
startNodesInProcess: Boolean = defaultParameters.startNodesInProcess, startNodesInProcess: Boolean = defaultParameters.startNodesInProcess,
notarySpecs: List<NotarySpec>, notarySpecs: List<NotarySpec> = defaultParameters.notarySpecs,
extraCordappPackagesToScan: List<String> = defaultParameters.extraCordappPackagesToScan, extraCordappPackagesToScan: List<String> = defaultParameters.extraCordappPackagesToScan,
dsl: SpringDriverExposedDSLInterface.() -> A dsl: SpringDriverExposedDSLInterface.() -> A
) = genericDriver( ) = genericDriver(

View File

@ -36,7 +36,7 @@ import net.corda.nodeapi.config.toConfig
import net.corda.nodeapi.internal.NotaryInfo import net.corda.nodeapi.internal.NotaryInfo
import net.corda.nodeapi.internal.addShutdownHook import net.corda.nodeapi.internal.addShutdownHook
import net.corda.testing.* import net.corda.testing.*
import net.corda.testing.common.internal.NetworkParametersCopier import net.corda.nodeapi.internal.NetworkParametersCopier
import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.internal.ProcessUtilities import net.corda.testing.internal.ProcessUtilities
import net.corda.testing.node.ClusterSpec import net.corda.testing.node.ClusterSpec

View File

@ -16,7 +16,7 @@ import net.corda.node.services.config.parseAsNodeConfiguration
import net.corda.node.services.config.plus import net.corda.node.services.config.plus
import net.corda.nodeapi.User import net.corda.nodeapi.User
import net.corda.testing.SerializationEnvironmentRule import net.corda.testing.SerializationEnvironmentRule
import net.corda.testing.common.internal.NetworkParametersCopier import net.corda.nodeapi.internal.NetworkParametersCopier
import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.driver.addressMustNotBeBoundFuture import net.corda.testing.driver.addressMustNotBeBoundFuture
import net.corda.testing.getFreeLocalPorts import net.corda.testing.getFreeLocalPorts

View File

@ -40,7 +40,7 @@ import net.corda.node.utilities.CordaPersistence
import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.node.utilities.ServiceIdentityGenerator
import net.corda.nodeapi.internal.NotaryInfo import net.corda.nodeapi.internal.NotaryInfo
import net.corda.testing.DUMMY_NOTARY import net.corda.testing.DUMMY_NOTARY
import net.corda.testing.common.internal.NetworkParametersCopier import net.corda.nodeapi.internal.NetworkParametersCopier
import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties

View File

@ -8,7 +8,7 @@ import net.corda.core.internal.createDirectories
import net.corda.core.internal.div import net.corda.core.internal.div
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.testing.common.internal.NetworkParametersCopier import net.corda.nodeapi.internal.NetworkParametersCopier
import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.common.internal.asContextEnv import net.corda.testing.common.internal.asContextEnv
import java.nio.file.Path import java.nio.file.Path