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
guavaVersion=21.0
bouncycastleVersion=1.57

View File

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

View File

@ -1,6 +1,5 @@
package net.corda.cordform;
import org.bouncycastle.asn1.x500.X500Name;
import java.nio.file.Path;
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 net.corda.cordform.CordformDefinition
import net.corda.cordform.CordformNode
import net.corda.cordform.NetworkParametersGenerator
import org.apache.tools.ant.filters.FixCrLfFilter
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.plugins.JavaPluginConvention
import org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME
import org.gradle.api.tasks.TaskAction
import java.io.File
import java.net.URLClassLoader
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.util.concurrent.TimeUnit
@ -28,6 +27,7 @@ open class Cordform : DefaultTask() {
*/
@Suppress("MemberVisibilityCanPrivate")
var definitionClass: String? = null
private val networkParametersGenClass: String = "net.corda.nodeapi.internal.TestNetworkParametersGenerator"
private var directory = Paths.get("build", "nodes")
private val nodes = mutableListOf<Node>()
@ -113,6 +113,19 @@ open class Cordform : DefaultTask() {
.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.
*/
@ -124,6 +137,7 @@ open class Cordform : DefaultTask() {
installRunScript()
nodes.forEach(Node::build)
generateAndInstallNodeInfos()
generateAndInstallNetworkParameters()
}
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() {
generateNodeInfos()
installNodeInfos()
@ -149,7 +169,7 @@ open class Cordform : DefaultTask() {
private fun generateNodeInfos() {
project.logger.info("Generating node infos")
var nodeProcesses = buildNodeProcesses()
val nodeProcesses = buildNodeProcesses()
try {
validateNodeProcessess(nodeProcesses)
} finally {
@ -158,9 +178,10 @@ open class Cordform : DefaultTask() {
}
private fun buildNodeProcesses(): Map<Node, Process> {
return nodes
.map { buildNodeProcess(it) }
.toMap()
val command = generateNodeInfoCommand()
return nodes.map {
it.makeLogDirectory()
buildProcess(it, command, "generate-info.log") }.toMap()
}
private fun validateNodeProcessess(nodeProcesses: Map<Node, Process>) {
@ -175,14 +196,13 @@ open class Cordform : DefaultTask() {
}
}
private fun buildNodeProcess(node: Node): Pair<Node, Process> {
node.makeLogDirectory()
var process = ProcessBuilder(generateNodeInfoCommand())
private fun buildProcess(node: Node, command: List<String>, logFile: String): Pair<Node, Process> {
val process = ProcessBuilder(command)
.directory(node.fullPath().toFile())
.redirectErrorStream(true)
// 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)
.redirectOutput(node.logFile().toFile())
.redirectOutput(node.logFile(logFile).toFile())
.addEnvironment("CAPSULE_CACHE_DIR", Node.capsuleCacheDir)
.start()
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) }
}

View File

@ -2,9 +2,6 @@ package net.corda.plugins
import com.typesafe.config.*
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 java.io.File
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")
throw IllegalStateException("Node has a null name - cannot create node")
}
val dirName = try {
val o = X500Name(name).getRDNs(BCStyle.O)
if (o.size > 0) {
o.first().first.value.toString()
} else {
name
}
} catch(_ : IllegalArgumentException) {
// Can't parse as an X500 name, use the full string
name
}
// Parsing O= part directly because importing BouncyCastle provider in Cordformation causes problems
// with loading our custom X509EdDSAEngine.
val organizationName = name.trim().split(",").firstOrNull { it.startsWith("O=") }?.substringAfter("=")
val dirName = organizationName ?: name
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.
*/
private fun installCordaJar() {
val cordaJar = verifyAndGetCordaJar()
val cordaJar = verifyAndGetRuntimeJar("corda")
project.copy {
it.apply {
from(cordaJar)
@ -166,7 +155,7 @@ class Node(private val project: Project) : CordformNode() {
* Installs the corda webserver JAR to the node directory
*/
private fun installWebserverJar() {
val webJar = verifyAndGetWebserverJar()
val webJar = verifyAndGetRuntimeJar("corda-webserver")
project.copy {
it.apply {
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 {
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 {
private fun verifyAndGetRuntimeJar(jarName: String): File {
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) {
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 {
val jar = maybeJar.singleFile
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.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.nodeapi.internal.NotaryInfo
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.contracts.DummyContract
import net.corda.testing.dummyCommand

View File

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

View File

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

View File

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

View File

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

View File

@ -36,7 +36,7 @@ import net.corda.nodeapi.config.toConfig
import net.corda.nodeapi.internal.NotaryInfo
import net.corda.nodeapi.internal.addShutdownHook
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.internal.ProcessUtilities
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.nodeapi.User
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.driver.addressMustNotBeBoundFuture
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.nodeapi.internal.NotaryInfo
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.node.MockServices.Companion.MOCK_VERSION_INFO
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.utilities.NetworkHostAndPort
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.asContextEnv
import java.nio.file.Path