diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 8dfe4ee76b..4af7873898 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -152,11 +152,8 @@
-
-
-
-
-
+
+
diff --git a/build.gradle b/build.gradle
index 833ded0724..c129f56dfc 100644
--- a/build.gradle
+++ b/build.gradle
@@ -25,9 +25,9 @@ buildscript {
//
// TODO: Sort this alphabetically.
ext.kotlin_version = constants.getProperty("kotlinVersion")
- // use our fork of quasar
- ext.quasar_group = 'com.github.corda.quasar'
- ext.quasar_version = '7629695563deae6cc95adcfbebcbc8322fd0241a'
+
+ ext.quasar_group = 'co.paralleluniverse'
+ ext.quasar_version = '0.7.10'
// gradle-capsule-plugin:1.0.2 contains capsule:1.0.1
// TODO: Upgrade gradle-capsule-plugin to a version with capsule:1.0.3
@@ -405,12 +405,12 @@ bintrayConfig {
'corda-shell',
'corda-serialization',
'corda-serialization-deterministic',
+ 'corda-tools-blob-inspector',
+ 'corda-tools-network-bootstrapper',
'corda-bridgeserver',
'corda-ptflows',
'jmeter-corda',
- 'tools-database-manager',
- 'tools-blob-inspector',
- 'tools-network-bootstrapper'
+ 'tools-database-manager'
]
license {
name = 'Apache-2.0'
diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst
index fa47f114ae..9bb0fc3798 100644
--- a/docs/source/changelog.rst
+++ b/docs/source/changelog.rst
@@ -8,6 +8,8 @@ Unreleased
==========
* Introduced a hierarchy of ``DatabaseMigrationException``s, allowing ``NodeStartup`` to gracefully inform users of problems related to database migrations before exiting with a non-zero code.
+* Add ``devModeOptions.allowCompatibilityZone`` to re-enable the use of a compatibility zone and ``devMode``
+
* Fixed an issue where ``trackBy`` was returning ``ContractStates`` from a transaction that were not being tracked. The
unrelated ``ContractStates`` will now be filtered out from the returned ``Vault.Update``.
@@ -151,7 +153,7 @@ Version 3.1
* Update the fast-classpath-scanner dependent library version from 2.0.21 to 2.12.3
-* Added `database.hibernateDialect` node configuration option
+ * Added `database.hibernateDialect` node configuration option
.. _changelog_r3_v3:
diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst
index 2f1b0ddbac..813d5f0341 100644
--- a/docs/source/corda-configuration-file.rst
+++ b/docs/source/corda-configuration-file.rst
@@ -205,7 +205,13 @@ absolute path to the node's base directory.
:doormanURL: Root address of the network registration service.
:networkMapURL: Root address of the network map service.
-.. note:: Only one of ``compatibilityZoneURL`` or ``networkServices`` should be used.
+ .. note:: Only one of ``compatibilityZoneURL`` or ``networkServices`` should be used.
+
+:devModeOptions: Allows modification of certain ``devMode`` features
+
+ :allowCompatibilityZone: Allows a node configured to operate in development mode to connect to a compatibility zone.
+
+ .. note:: This is an unsupported configuration.
:jvmArgs: An optional list of JVM args, as strings, which replace those inherited from the command line when launching via ``corda.jar``
only. e.g. ``jvmArgs = [ "-Xmx220m", "-Xms220m", "-XX:+UseG1GC" ]``
diff --git a/docs/source/generating-a-node.rst b/docs/source/generating-a-node.rst
index a457835924..59a9b1e1e5 100644
--- a/docs/source/generating-a-node.rst
+++ b/docs/source/generating-a-node.rst
@@ -170,25 +170,25 @@ Below you can find the example task from the `IRS Demo ) {
@@ -301,35 +299,6 @@ class NetworkBootstrapper {
return networkParameters
}
- @VisibleForTesting
- internal fun generateWhitelist(networkParameters: NetworkParameters?,
- excludeContracts: List,
- cordappJars: List): Map> {
- val existingWhitelist = networkParameters?.whitelistedContractImplementations ?: emptyMap()
-
- if (excludeContracts.isNotEmpty()) {
- println("Exclude contracts from whitelist: ${excludeContracts.joinToString()}")
- existingWhitelist.keys.forEach {
- require(it !in excludeContracts) { "$it is already part of the existing whitelist and cannot be excluded." }
- }
- }
-
- val newWhiteList = cordappJars
- .flatMap { jar -> (jar.scan() - excludeContracts).map { it to jar.hash } }
- .toMultiMap()
-
- return (newWhiteList.keys + existingWhitelist.keys).associateBy({ it }) {
- val existingHashes = existingWhitelist[it] ?: emptyList()
- val newHashes = newWhiteList[it] ?: emptyList()
- (existingHashes + newHashes).distinct()
- }
- }
-
- private fun readExcludeWhitelist(directory: Path): List {
- val file = directory / EXCLUDE_WHITELIST_FILE_NAME
- return if (file.exists()) file.readAllLines().map(String::trim) else emptyList()
- }
-
private fun NodeInfo.notaryIdentity(): Party {
return when (legalIdentities.size) {
// Single node notaries have just one identity like all other nodes. This identity is the notary identity
diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/WhitelisGenerator.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/WhitelisGenerator.kt
new file mode 100644
index 0000000000..9979337386
--- /dev/null
+++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/WhitelisGenerator.kt
@@ -0,0 +1,43 @@
+package net.corda.nodeapi.internal.network
+
+import net.corda.core.contracts.ContractClassName
+import net.corda.core.internal.div
+import net.corda.core.internal.exists
+import net.corda.core.internal.readAllLines
+import net.corda.core.internal.toMultiMap
+import net.corda.core.node.NetworkParameters
+import net.corda.core.node.services.AttachmentId
+import net.corda.nodeapi.internal.ContractsJar
+import org.slf4j.LoggerFactory
+import java.nio.file.Path
+
+private const val EXCLUDE_WHITELIST_FILE_NAME = "exclude_whitelist.txt"
+private val logger = LoggerFactory.getLogger("net.corda.nodeapi.internal.network.WhitelistGenerator")
+
+fun generateWhitelist(networkParameters: NetworkParameters?,
+ excludeContracts: List,
+ cordappJars: List): Map> {
+ val existingWhitelist = networkParameters?.whitelistedContractImplementations ?: emptyMap()
+
+ if (excludeContracts.isNotEmpty()) {
+ logger.info("Exclude contracts from whitelist: ${excludeContracts.joinToString()}")
+ existingWhitelist.keys.forEach {
+ require(it !in excludeContracts) { "$it is already part of the existing whitelist and cannot be excluded." }
+ }
+ }
+
+ val newWhiteList = cordappJars
+ .flatMap { jar -> (jar.scan() - excludeContracts).map { it to jar.hash } }
+ .toMultiMap()
+
+ return (newWhiteList.keys + existingWhitelist.keys).associateBy({ it }) {
+ val existingHashes = existingWhitelist[it] ?: emptyList()
+ val newHashes = newWhiteList[it] ?: emptyList()
+ (existingHashes + newHashes).distinct()
+ }
+}
+
+fun readExcludeWhitelist(directory: Path): List {
+ val file = directory / EXCLUDE_WHITELIST_FILE_NAME
+ return if (file.exists()) file.readAllLines().map(String::trim) else emptyList()
+}
\ No newline at end of file
diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt
index 59452aa0b2..b57c177d61 100644
--- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt
+++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt
@@ -127,7 +127,7 @@ class NetworkBootstrapperTest {
private fun generateWhitelist(existingWhitelist: Map>,
excludeContracts: List,
contractJars: List): Map> {
- return NetworkBootstrapper().generateWhitelist(
+ return generateWhitelist(
testNetworkParameters(whitelistedContractImplementations = existingWhitelist),
excludeContracts,
contractJars
diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/WhitelistGeneratorTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/WhitelistGeneratorTest.kt
new file mode 100644
index 0000000000..67f4bb9641
--- /dev/null
+++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/WhitelistGeneratorTest.kt
@@ -0,0 +1,41 @@
+package net.corda.nodeapi.internal.network
+
+import com.nhaarman.mockito_kotlin.mock
+import com.nhaarman.mockito_kotlin.verify
+import net.corda.core.crypto.SecureHash
+import net.corda.nodeapi.internal.ContractsJar
+import org.junit.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+
+class WhitelistGeneratorTest {
+
+ @Test
+ fun `whitelist generator builds the correct whitelist map`() {
+ // given
+ val jars = (0..9).map {
+ val index = it
+ mock {
+ val secureHash = SecureHash.randomSHA256()
+ on { scan() }.then {
+ listOf(index.toString())
+ }
+ on { hash }.then {
+ secureHash
+ }
+ }
+ }
+
+ // when
+ val result = generateWhitelist(null, emptyList(), jars)
+
+ // then
+ jars.forEachIndexed { index, item ->
+ verify(item).scan()
+ val attachmentIds = requireNotNull(result[index.toString()])
+ assertEquals(1, attachmentIds.size)
+ assertTrue { attachmentIds.contains(item.hash) }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt
index b6f232a34b..db90db75d7 100644
--- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt
+++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt
@@ -93,7 +93,7 @@ interface NodeConfiguration : NodeSSLConfiguration {
}
}
-data class DevModeOptions(val disableCheckpointChecker: Boolean = false)
+data class DevModeOptions(val disableCheckpointChecker: Boolean = false, val allowCompatibilityZone: Boolean = false)
data class GraphiteOptions(
val server: String,
@@ -314,16 +314,19 @@ data class NodeConfigurationImpl(
private fun validateDevModeOptions(): List {
if (devMode) {
compatibilityZoneURL?.let {
- return listOf("'compatibilityZoneURL': present. Property cannot be set when 'devMode' is true.")
+ if (devModeOptions?.allowCompatibilityZone != true) {
+ return listOf("'compatibilityZoneURL': present. Property cannot be set when 'devMode' is true unless devModeOptions.allowCompatibilityZone is also true")
+ }
}
// if compatibiliZoneURL is set then it will be copied into the networkServices field and thus skipping
// this check by returning above is fine.
networkServices?.let {
- return listOf("'networkServices': present. Property cannot be set when 'devMode' is true.")
+ if (devModeOptions?.allowCompatibilityZone != true) {
+ return listOf("'networkServices': present. Property cannot be set when 'devMode' is true unless devModeOptions.allowCompatibilityZone is also true")
+ }
}
}
-
return emptyList()
}
diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt
index 5f62db0917..743e77f093 100644
--- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt
+++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt
@@ -37,6 +37,7 @@ import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseTransaction
import net.corda.nodeapi.internal.persistence.contextTransaction
import net.corda.nodeapi.internal.persistence.contextTransactionOrNull
+import org.apache.activemq.artemis.utils.ReusableLatch
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.slf4j.MDC
@@ -78,7 +79,8 @@ class FlowStateMachineImpl(override val id: StateMachineRunId,
val actionExecutor: ActionExecutor,
val stateMachine: StateMachine,
val serviceHub: ServiceHubInternal,
- val checkpointSerializationContext: SerializationContext
+ val checkpointSerializationContext: SerializationContext,
+ val unfinishedFibers: ReusableLatch
)
internal var transientValues: TransientReference? = null
@@ -249,6 +251,7 @@ class FlowStateMachineImpl(override val id: StateMachineRunId,
}
recordDuration(startTime)
+ getTransientField(TransientValues::unfinishedFibers).countDown()
}
@Suspendable
diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/SingleThreadedStateMachineManager.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/SingleThreadedStateMachineManager.kt
index e5695fea15..e664ffc73a 100644
--- a/node/src/main/kotlin/net/corda/node/services/statemachine/SingleThreadedStateMachineManager.kt
+++ b/node/src/main/kotlin/net/corda/node/services/statemachine/SingleThreadedStateMachineManager.kt
@@ -23,20 +23,12 @@ import net.corda.core.flows.FlowInfo
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StateMachineRunId
import net.corda.core.identity.Party
-import net.corda.core.internal.FlowStateMachine
-import net.corda.core.internal.ThreadBox
-import net.corda.core.internal.TimedFlow
-import net.corda.core.internal.bufferUntilSubscribed
-import net.corda.core.internal.castIfPossible
+import net.corda.core.internal.*
import net.corda.core.internal.concurrent.OpenFuture
import net.corda.core.internal.concurrent.map
import net.corda.core.internal.concurrent.openFuture
import net.corda.core.messaging.DataFeed
-import net.corda.core.serialization.SerializationContext
-import net.corda.core.serialization.SerializationDefaults
-import net.corda.core.serialization.SerializedBytes
-import net.corda.core.serialization.deserialize
-import net.corda.core.serialization.serialize
+import net.corda.core.serialization.*
import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.Try
import net.corda.core.utilities.contextLogger
@@ -48,11 +40,7 @@ import net.corda.node.services.config.shouldCheckCheckpoints
import net.corda.node.services.messaging.DeduplicationHandler
import net.corda.node.services.messaging.ReceivedMessage
import net.corda.node.services.statemachine.FlowStateMachineImpl.Companion.createSubFlowVersion
-import net.corda.node.services.statemachine.interceptors.DumpHistoryOnErrorInterceptor
-import net.corda.node.services.statemachine.interceptors.FiberDeserializationChecker
-import net.corda.node.services.statemachine.interceptors.FiberDeserializationCheckingInterceptor
-import net.corda.node.services.statemachine.interceptors.HospitalisingInterceptor
-import net.corda.node.services.statemachine.interceptors.PrintingInterceptor
+import net.corda.node.services.statemachine.interceptors.*
import net.corda.node.services.statemachine.transitions.StateMachine
import net.corda.node.utilities.AffinityExecutor
import net.corda.nodeapi.internal.persistence.CordaPersistence
@@ -64,16 +52,10 @@ import rx.Observable
import rx.subjects.PublishSubject
import java.security.SecureRandom
import java.util.*
-import java.util.concurrent.ConcurrentHashMap
-import java.util.concurrent.ExecutorService
-import java.util.concurrent.Executors
-import java.util.concurrent.ScheduledFuture
-import java.util.concurrent.TimeUnit
-import java.util.concurrent.locks.ReentrantLock
+import java.util.concurrent.*
import javax.annotation.concurrent.ThreadSafe
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
-import kotlin.concurrent.withLock
import kotlin.streams.toList
/**
@@ -238,7 +220,6 @@ class SingleThreadedStateMachineManager(
logger.debug("Killing flow known to physical node.")
decrementLiveFibers()
totalFinishedFlows.inc()
- unfinishedFibers.countDown()
try {
flow.fiber.interrupt()
true
@@ -247,6 +228,7 @@ class SingleThreadedStateMachineManager(
checkpointStorage.removeCheckpoint(id)
}
transitionExecutor.forceRemoveFlow(id)
+ unfinishedFibers.countDown()
}
} else {
// TODO replace with a clustered delete after we'll support clustered nodes
@@ -290,7 +272,6 @@ class SingleThreadedStateMachineManager(
if (flow != null) {
decrementLiveFibers()
totalFinishedFlows.inc()
- unfinishedFibers.countDown()
return when (removalReason) {
is FlowRemovalReason.OrderlyFinish -> removeFlowOrderly(flow, removalReason, lastState)
is FlowRemovalReason.ErrorFinish -> removeFlowError(flow, removalReason, lastState)
@@ -374,8 +355,7 @@ class SingleThreadedStateMachineManager(
checkpoint = checkpoint,
initialDeduplicationHandler = null,
isAnyCheckpointPersisted = true,
- isStartIdempotent = false,
- senderUUID = null
+ isStartIdempotent = false
)
} else {
// Just flow initiation message
@@ -671,7 +651,8 @@ class SingleThreadedStateMachineManager(
actionExecutor = actionExecutor!!,
stateMachine = StateMachine(id, secureRandom),
serviceHub = serviceHub,
- checkpointSerializationContext = checkpointSerializationContext!!
+ checkpointSerializationContext = checkpointSerializationContext!!,
+ unfinishedFibers = unfinishedFibers
)
}
@@ -680,8 +661,7 @@ class SingleThreadedStateMachineManager(
checkpoint: Checkpoint,
isAnyCheckpointPersisted: Boolean,
isStartIdempotent: Boolean,
- initialDeduplicationHandler: DeduplicationHandler?,
- senderUUID: String? = ourSenderUUID
+ initialDeduplicationHandler: DeduplicationHandler?
): Flow {
val flowState = checkpoint.flowState
val resultFuture = openFuture()
@@ -697,7 +677,7 @@ class SingleThreadedStateMachineManager(
isStartIdempotent = isStartIdempotent,
isRemoved = false,
flowLogic = logic,
- senderUUID = senderUUID
+ senderUUID = null
)
val fiber = FlowStateMachineImpl(id, logic, scheduler)
fiber.transientValues = TransientReference(createTransientValues(id, resultFuture))
@@ -716,7 +696,7 @@ class SingleThreadedStateMachineManager(
isStartIdempotent = isStartIdempotent,
isRemoved = false,
flowLogic = fiber.logic,
- senderUUID = senderUUID
+ senderUUID = null
)
fiber.transientValues = TransientReference(createTransientValues(id, resultFuture))
fiber.transientState = TransientReference(state)
diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StaffedFlowHospital.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StaffedFlowHospital.kt
index 4228a73367..0a2e70ce6f 100644
--- a/node/src/main/kotlin/net/corda/node/services/statemachine/StaffedFlowHospital.kt
+++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StaffedFlowHospital.kt
@@ -206,7 +206,11 @@ class StaffedFlowHospital {
if (history.notDischargedForTheSameThingMoreThan(newError.maxRetries, this)) {
return Diagnosis.DISCHARGE
} else {
- log.warn("\"Maximum number of retries reached for timed flow ${flowFiber.javaClass}")
+ val errorMsg = "Maximum number of retries reached for flow ${flowFiber.snapshot().flowLogic.javaClass}." +
+ "If the flow involves notarising a transaction, this usually means that the notary is being overloaded and" +
+ "unable to service requests fast enough. Please try again later."
+ newError.setMessage(errorMsg)
+ log.warn(errorMsg)
}
}
return Diagnosis.NOT_MY_SPECIALTY
diff --git a/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt b/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt
index 1b58ffcc38..76a9a45cc6 100644
--- a/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt
+++ b/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt
@@ -10,12 +10,8 @@
package net.corda.node.services.config
-import com.typesafe.config.Config
-import com.typesafe.config.ConfigException
-import com.typesafe.config.ConfigFactory
+import com.typesafe.config.*
import com.zaxxer.hikari.HikariConfig
-import com.typesafe.config.ConfigParseOptions
-import com.typesafe.config.ConfigValueFactory
import net.corda.core.internal.toPath
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.nodeapi.internal.persistence.CordaPersistence.DataSourceConfigTag
@@ -24,11 +20,9 @@ import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
import net.corda.tools.shell.SSHDConfiguration
-import org.assertj.core.api.Assertions.assertThat
-import org.assertj.core.api.Assertions.assertThatCode
-import org.assertj.core.api.Assertions.assertThatThrownBy
-import org.junit.Assert.assertNotNull
+import org.assertj.core.api.Assertions.*
import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
import org.junit.Test
import java.net.InetAddress
import java.net.URL
@@ -179,6 +173,17 @@ class NodeConfigurationImplTest {
assertThat(errors).hasOnlyOneElementSatisfying { error -> error.contains("compatibilityZoneURL") && error.contains("devMode") }
}
+ @Test
+ fun `validation succeeds when compatibilityZoneURL is present and devMode is true and allowCompatibilityZoneURL is set`() {
+ val configuration = testConfiguration.copy(
+ devMode = true,
+ compatibilityZoneURL = URL("https://r3.com"),
+ devModeOptions = DevModeOptions(allowCompatibilityZone = true))
+
+ val errors = configuration.validate()
+ assertThat(errors).isEmpty()
+ }
+
@Test
fun `mutual exclusion machineName set to default if not explicitly set`() {
val config = getConfig("test-config-mutualExclusion-noMachineName.conf").parseAsNodeConfiguration(UnknownConfigKeysPolicy.IGNORE::handle)
diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/RetryFlowMockTest.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/RetryFlowMockTest.kt
index 56d009eeef..42d79ce2a8 100644
--- a/node/src/test/kotlin/net/corda/node/services/statemachine/RetryFlowMockTest.kt
+++ b/node/src/test/kotlin/net/corda/node/services/statemachine/RetryFlowMockTest.kt
@@ -29,6 +29,7 @@ import org.junit.Before
import org.junit.Test
import java.sql.SQLException
import java.time.Duration
+import java.util.*
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
@@ -47,6 +48,7 @@ class RetryFlowMockTest {
RetryFlow.count = 0
SendAndRetryFlow.count = 0
RetryInsertFlow.count = 0
+ KeepSendingFlow.count = 0
}
private fun StartedNode.startFlow(logic: FlowLogic): CordaFuture {
@@ -74,7 +76,7 @@ class RetryFlowMockTest {
@Test
fun `Retry does not set senderUUID`() {
- val messagesSent = mutableListOf()
+ val messagesSent = Collections.synchronizedList(mutableListOf())
val partyB = nodeB.info.legalIdentities.first()
nodeA.setMessagingServiceSpy(object : MessagingServiceSpy(nodeA.network) {
override fun send(message: Message, target: MessageRecipients, sequenceKey: Any) {
@@ -88,6 +90,38 @@ class RetryFlowMockTest {
assertEquals(2, SendAndRetryFlow.count)
}
+ @Test
+ fun `Restart does not set senderUUID`() {
+ val messagesSent = Collections.synchronizedList(mutableListOf())
+ val partyB = nodeB.info.legalIdentities.first()
+ nodeA.setMessagingServiceSpy(object : MessagingServiceSpy(nodeA.network) {
+ override fun send(message: Message, target: MessageRecipients, sequenceKey: Any) {
+ messagesSent.add(message)
+ messagingService.send(message, target)
+ }
+ })
+ val count = 10000 // Lots of iterations so the flow keeps going long enough
+ nodeA.startFlow(KeepSendingFlow(count, partyB))
+ while (messagesSent.size < 1) {
+ Thread.sleep(10)
+ }
+ assertNotNull(messagesSent.first().senderUUID)
+ nodeA = mockNet.restartNode(nodeA)
+ // This is a bit racy because restarting the node actually starts it, so we need to make sure there's enough iterations we get here with flow still going.
+ nodeA.setMessagingServiceSpy(object : MessagingServiceSpy(nodeA.network) {
+ override fun send(message: Message, target: MessageRecipients, sequenceKey: Any) {
+ messagesSent.add(message)
+ messagingService.send(message, target)
+ }
+ })
+ // Now short circuit the iterations so the flow finishes soon.
+ KeepSendingFlow.count = count - 2
+ while (nodeA.smm.allStateMachines.size > 0) {
+ Thread.sleep(10)
+ }
+ assertNull(messagesSent.last().senderUUID)
+ }
+
@Test
fun `Retry duplicate insert`() {
assertEquals(Unit, nodeA.startFlow(RetryInsertFlow(1)).get())
@@ -105,6 +139,8 @@ class RetryFlowMockTest {
@Test
fun `Patient records do not leak in hospital when using killFlow`() {
+ // Make sure we have seen an update from the hospital, and thus the flow went there.
+ val records = nodeA.smm.flowHospital.track().updates.toBlocking().toIterable().iterator()
val flow: FlowStateMachine = nodeA.services.startFlow(FinalityHandler(object : FlowSession() {
override val counterparty: Party
get() = TODO("not implemented")
@@ -141,8 +177,9 @@ class RetryFlowMockTest {
TODO("not implemented")
}
}), nodeA.services.newContext()).get()
- // Make sure we have seen an update from the hospital, and thus the flow went there.
- nodeA.smm.flowHospital.track().updates.toBlocking().first()
+ // Should be 2 records, one for admission and one for keep in.
+ records.next()
+ records.next()
// Killing it should remove it.
nodeA.smm.killFlow(flow.id)
assertThat(nodeA.smm.flowHospital.track().snapshot).isEmpty()
@@ -155,6 +192,7 @@ class RetryCausingError : SQLException("deadlock")
class RetryFlow(private val i: Int) : FlowLogic() {
companion object {
+ @Volatile
var count = 0
}
@@ -171,29 +209,10 @@ class RetryFlow(private val i: Int) : FlowLogic() {
}
}
-class RetryAndSleepFlow(private val i: Int) : FlowLogic() {
- companion object {
- var count = 0
- }
-
- @Suspendable
- override fun call() {
- logger.info("Hello $count")
- if (count++ < i) {
- if (i == Int.MAX_VALUE) {
- throw LimitedRetryCausingError()
- } else {
- throw RetryCausingError()
- }
- } else {
- sleep(Duration.ofDays(1))
- }
- }
-}
-
@InitiatingFlow
class SendAndRetryFlow(private val i: Int, private val other: Party) : FlowLogic() {
companion object {
+ @Volatile
var count = 0
}
@@ -218,8 +237,40 @@ class ReceiveFlow2(private val other: FlowSession) : FlowLogic() {
}
}
+@InitiatingFlow
+class KeepSendingFlow(private val i: Int, private val other: Party) : FlowLogic() {
+ companion object {
+ @Volatile
+ var count = 0
+ }
+
+ @Suspendable
+ override fun call() {
+ val session = initiateFlow(other)
+ session.send(i.toString())
+ do {
+ logger.info("Sending... $count")
+ session.send("Boo")
+ } while (count++ < i)
+ }
+}
+
+@Suppress("unused")
+@InitiatedBy(KeepSendingFlow::class)
+class ReceiveFlow3(private val other: FlowSession) : FlowLogic() {
+ @Suspendable
+ override fun call() {
+ var count = other.receive().unwrap { it.toInt() }
+ while (count-- > 0) {
+ val received = other.receive().unwrap { it }
+ logger.info("Received... $received $count")
+ }
+ }
+}
+
class RetryInsertFlow(private val i: Int) : FlowLogic() {
companion object {
+ @Volatile
var count = 0
}
diff --git a/settings.gradle b/settings.gradle
index 8407af74ce..e96990ff52 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -63,6 +63,7 @@ include 'tools:bootstrapper'
include 'tools:blobinspector'
include 'tools:dbmigration'
include 'tools:shell'
+include 'tools:network-bootstrapper'
include 'example-code'
project(':example-code').projectDir = file("$settingsDir/docs/source/example-code")
include 'samples:attachment-demo'
diff --git a/tools/blobinspector/build.gradle b/tools/blobinspector/build.gradle
index 06b4f21e52..c20270831d 100644
--- a/tools/blobinspector/build.gradle
+++ b/tools/blobinspector/build.gradle
@@ -20,7 +20,7 @@ jar {
exclude "META-INF/*.DSA"
exclude "META-INF/*.RSA"
}
- archiveName = "blob-inspector-${version}.jar"
+ archiveName = "blob-inspector-${corda_release_version}.jar"
manifest {
attributes(
'Automatic-Module-Name': 'net.corda.blobinspector',
@@ -30,5 +30,5 @@ jar {
}
publish {
- name 'tools-blob-inspector'
+ name 'corda-tools-blob-inspector'
}
diff --git a/tools/bootstrapper/build.gradle b/tools/bootstrapper/build.gradle
index 057229c099..de882bb285 100644
--- a/tools/bootstrapper/build.gradle
+++ b/tools/bootstrapper/build.gradle
@@ -18,13 +18,17 @@ configurations {
runtimeArtifacts
}
+jar {
+ baseName "corda-tools-network-bootstrapper"
+}
+
dependencies {
compile "org.slf4j:slf4j-nop:$slf4j_version"
}
task buildBootstrapperJar(type: FatCapsule, dependsOn: project(':node-api').compileJava) {
applicationClass 'net.corda.nodeapi.internal.network.NetworkBootstrapper'
- archiveName = "network-bootstrapper-${version}.jar"
+ archiveName "tools-network-bootstrapper-${corda_release_version}.jar"
capsuleManifest {
applicationVersion = corda_release_version
systemProperties['visualvm.display.name'] = 'Network Bootstrapper'
@@ -48,6 +52,6 @@ artifacts {
}
publish {
- name 'tools-network-bootstrapper'
disableDefaultJar = true
+ name 'corda-tools-network-bootstrapper'
}
diff --git a/tools/network-bootstrapper/build.gradle b/tools/network-bootstrapper/build.gradle
new file mode 100644
index 0000000000..7cc76f0695
--- /dev/null
+++ b/tools/network-bootstrapper/build.gradle
@@ -0,0 +1,71 @@
+buildscript {
+
+ ext.tornadofx_version = '1.7.15'
+ ext.controlsfx_version = '8.40.12'
+
+
+ repositories {
+ mavenLocal()
+ mavenCentral()
+ jcenter()
+ }
+
+ dependencies {
+ classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version"
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.1'
+ }
+}
+
+repositories {
+ mavenLocal()
+ mavenCentral()
+ jcenter()
+}
+
+
+apply plugin: 'kotlin'
+apply plugin: 'idea'
+apply plugin: 'java'
+apply plugin: 'application'
+apply plugin: 'com.github.johnrengelman.shadow'
+
+dependencies {
+
+ compile "com.microsoft.azure:azure:1.8.0"
+ compile "com.github.docker-java:docker-java:3.0.6"
+
+ testCompile "org.jetbrains.kotlin:kotlin-test"
+ testCompile "org.jetbrains.kotlin:kotlin-test-junit"
+
+ compile project(':node-api')
+ compile project(':node')
+
+ compile group: "com.typesafe", name: "config", version: typesafe_config_version
+ compile group: "com.fasterxml.jackson.dataformat", name: "jackson-dataformat-yaml", version: "2.9.0"
+ compile group: "com.fasterxml.jackson.core", name: "jackson-databind", version: "2.9.0"
+ compile "com.fasterxml.jackson.module:jackson-module-kotlin:2.9.+"
+ compile group: 'info.picocli', name: 'picocli', version: '3.0.1'
+
+ // TornadoFX: A lightweight Kotlin framework for working with JavaFX UI's.
+ compile "no.tornado:tornadofx:$tornadofx_version"
+
+ compile "org.controlsfx:controlsfx:$controlsfx_version"
+
+}
+
+shadowJar {
+ baseName = 'network-bootstrapper'
+ classifier = null
+ version = null
+ zip64 true
+ mainClassName = 'net.corda.bootstrapper.Main'
+}
+
+task buildNetworkBootstrapper(dependsOn: shadowJar) {
+}
+
+configurations {
+ compile.exclude group: "log4j", module: "log4j"
+ compile.exclude group: "org.apache.logging.log4j"
+}
\ No newline at end of file
diff --git a/tools/network-bootstrapper/src/main/java/net/corda/bootstrapper/GuiUtils.java b/tools/network-bootstrapper/src/main/java/net/corda/bootstrapper/GuiUtils.java
new file mode 100644
index 0000000000..204a9529fa
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/java/net/corda/bootstrapper/GuiUtils.java
@@ -0,0 +1,48 @@
+package net.corda.bootstrapper;
+
+import javafx.scene.control.Alert;
+import javafx.scene.control.Label;
+import javafx.scene.control.TextArea;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.Priority;
+import javafx.stage.StageStyle;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+public class GuiUtils {
+
+ public static void showException(String title, String message, Throwable exception) {
+ Alert alert = new Alert(Alert.AlertType.ERROR);
+ alert.initStyle(StageStyle.UTILITY);
+ alert.setTitle("Exception");
+ alert.setHeaderText(title);
+ alert.setContentText(message);
+
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ exception.printStackTrace(pw);
+ String exceptionText = sw.toString();
+
+ Label label = new Label("Details:");
+
+ TextArea textArea = new TextArea(exceptionText);
+ textArea.setEditable(false);
+ textArea.setWrapText(true);
+
+ textArea.setMaxWidth(Double.MAX_VALUE);
+ textArea.setMaxHeight(Double.MAX_VALUE);
+ GridPane.setVgrow(textArea, Priority.ALWAYS);
+ GridPane.setHgrow(textArea, Priority.ALWAYS);
+
+ GridPane expContent = new GridPane();
+ expContent.setMaxWidth(Double.MAX_VALUE);
+ expContent.add(label, 0, 0);
+ expContent.add(textArea, 0, 1);
+
+ alert.getDialogPane().setExpandableContent(expContent);
+
+ alert.showAndWait();
+ }
+
+}
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/Constants.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/Constants.kt
new file mode 100644
index 0000000000..d6e907d1b5
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/Constants.kt
@@ -0,0 +1,50 @@
+package net.corda.bootstrapper
+
+import com.fasterxml.jackson.core.JsonGenerator
+import com.fasterxml.jackson.core.JsonParser
+import com.fasterxml.jackson.databind.*
+import com.fasterxml.jackson.databind.module.SimpleModule
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
+import com.fasterxml.jackson.module.kotlin.registerKotlinModule
+import com.microsoft.azure.management.resources.ResourceGroup
+import com.microsoft.azure.management.resources.fluentcore.arm.Region
+
+class Constants {
+
+ companion object {
+ val NODE_P2P_PORT = 10020
+ val NODE_SSHD_PORT = 12222
+ val NODE_RPC_PORT = 10003
+ val NODE_RPC_ADMIN_PORT = 10005
+
+ val BOOTSTRAPPER_DIR_NAME = ".bootstrapper"
+
+ fun getContextMapper(): ObjectMapper {
+ val objectMapper = ObjectMapper(YAMLFactory()).registerKotlinModule()
+ objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
+ objectMapper.registerModule(object : SimpleModule() {}.let {
+ it.addSerializer(Region::class.java, object : JsonSerializer() {
+ override fun serialize(value: Region, gen: JsonGenerator, serializers: SerializerProvider?) {
+ gen.writeString(value.name())
+ }
+ })
+ it.addDeserializer(Region::class.java, object : JsonDeserializer() {
+ override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Region {
+ return Region.findByLabelOrName(p.valueAsString)
+ }
+ })
+ })
+ return objectMapper
+ }
+
+ val ALPHA_NUMERIC_ONLY_REGEX = "[^\\p{IsAlphabetic}\\p{IsDigit}]".toRegex()
+ val ALPHA_NUMERIC_DOT_AND_UNDERSCORE_ONLY_REGEX = "[^\\p{IsAlphabetic}\\p{IsDigit}._]".toRegex()
+ val REGION_ARG_NAME = "REGION"
+
+ fun ResourceGroup.restFriendlyName(): String {
+ return this.name().replace(ALPHA_NUMERIC_ONLY_REGEX, "").toLowerCase()
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt
new file mode 100644
index 0000000000..38a5539876
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt
@@ -0,0 +1,44 @@
+@file:JvmName("Main")
+
+package net.corda.bootstrapper
+
+import net.corda.bootstrapper.backends.Backend
+import net.corda.bootstrapper.backends.Backend.BackendType.AZURE
+import net.corda.bootstrapper.cli.AzureParser
+import net.corda.bootstrapper.cli.CliParser
+import net.corda.bootstrapper.cli.CommandLineInterface
+import net.corda.bootstrapper.cli.GuiSwitch
+import net.corda.bootstrapper.gui.Gui
+import net.corda.bootstrapper.serialization.SerializationEngine
+import picocli.CommandLine
+
+
+fun main(args: Array) {
+ SerializationEngine.init()
+
+ val entryPointArgs = GuiSwitch();
+ CommandLine(entryPointArgs).parse(*args)
+
+ if (entryPointArgs.usageHelpRequested) {
+ CommandLine.usage(AzureParser(), System.out)
+ return
+ }
+
+
+ if (entryPointArgs.gui) {
+ Gui.main(args)
+ } else {
+ val baseArgs = CliParser()
+ CommandLine(baseArgs).parse(*args)
+ val argParser: CliParser = when (baseArgs.backendType) {
+ AZURE -> {
+ val azureArgs = AzureParser()
+ CommandLine(azureArgs).parse(*args)
+ azureArgs
+ }
+ Backend.BackendType.LOCAL_DOCKER -> baseArgs
+ }
+ CommandLineInterface().run(argParser)
+ }
+
+}
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/NetworkBuilder.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/NetworkBuilder.kt
new file mode 100644
index 0000000000..f4f36d0381
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/NetworkBuilder.kt
@@ -0,0 +1,202 @@
+package net.corda.bootstrapper
+
+import net.corda.bootstrapper.backends.Backend
+import net.corda.bootstrapper.context.Context
+import net.corda.bootstrapper.nodes.*
+import net.corda.bootstrapper.notaries.NotaryCopier
+import net.corda.bootstrapper.notaries.NotaryFinder
+import java.io.File
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.ConcurrentHashMap
+
+interface NetworkBuilder {
+
+ companion object {
+ fun instance(): NetworkBuilder {
+ return NetworkBuilderImpl()
+ }
+ }
+
+ fun onNodeLocated(callback: (FoundNode) -> Unit): NetworkBuilder
+ fun onNodeCopied(callback: (CopiedNode) -> Unit): NetworkBuilder
+ fun onNodeBuild(callback: (BuiltNode) -> Unit): NetworkBuilder
+ fun onNodePushed(callback: (PushedNode) -> Unit): NetworkBuilder
+ fun onNodeInstance(callback: (NodeInstance) -> Unit): NetworkBuilder
+
+ fun withNodeCounts(map: Map): NetworkBuilder
+ fun withNetworkName(networtName: String): NetworkBuilder
+ fun withBasedir(baseDir: File): NetworkBuilder
+ fun withBackend(backendType: Backend.BackendType): NetworkBuilder
+ fun withBackendOptions(options: Map): NetworkBuilder
+
+ fun build(): CompletableFuture, Context>>
+}
+
+private class NetworkBuilderImpl : NetworkBuilder {
+
+
+ @Volatile
+ private var onNodeLocatedCallback: ((FoundNode) -> Unit) = {}
+ @Volatile
+ private var onNodeCopiedCallback: ((CopiedNode) -> Unit) = {}
+ @Volatile
+ private var onNodeBuiltCallback: ((BuiltNode) -> Unit) = {}
+ @Volatile
+ private var onNodePushedCallback: ((PushedNode) -> Unit) = {}
+ @Volatile
+ private var onNodeInstanceCallback: ((NodeInstance) -> Unit) = {}
+ @Volatile
+ private var nodeCounts = mapOf()
+ @Volatile
+ private lateinit var networkName: String
+ @Volatile
+ private var workingDir: File? = null
+ private val cacheDirName = Constants.BOOTSTRAPPER_DIR_NAME
+ @Volatile
+ private var backendType = Backend.BackendType.LOCAL_DOCKER
+ @Volatile
+ private var backendOptions: Map = mapOf()
+
+ override fun onNodeLocated(callback: (FoundNode) -> Unit): NetworkBuilder {
+ this.onNodeLocatedCallback = callback
+ return this
+ }
+
+ override fun onNodeCopied(callback: (CopiedNode) -> Unit): NetworkBuilder {
+ this.onNodeCopiedCallback = callback
+ return this
+ }
+
+ override fun onNodeBuild(callback: (BuiltNode) -> Unit): NetworkBuilder {
+ this.onNodeBuiltCallback = callback
+ return this
+ }
+
+ override fun onNodePushed(callback: (PushedNode) -> Unit): NetworkBuilder {
+ this.onNodePushedCallback = callback
+ return this
+ }
+
+ override fun onNodeInstance(callback: (NodeInstance) -> Unit): NetworkBuilder {
+ this.onNodeInstanceCallback = callback;
+ return this
+ }
+
+ override fun withNodeCounts(map: Map): NetworkBuilder {
+ nodeCounts = ConcurrentHashMap(map.entries.map { it.key.toLowerCase() to it.value }.toMap())
+ return this
+ }
+
+ override fun withNetworkName(networtName: String): NetworkBuilder {
+ this.networkName = networtName
+ return this
+ }
+
+ override fun withBasedir(baseDir: File): NetworkBuilder {
+ this.workingDir = baseDir
+ return this
+ }
+
+ override fun withBackend(backendType: Backend.BackendType): NetworkBuilder {
+ this.backendType = backendType
+ return this
+ }
+
+ override fun withBackendOptions(options: Map): NetworkBuilder {
+ this.backendOptions = HashMap(options)
+ return this
+ }
+
+ override fun build(): CompletableFuture, Context>> {
+ val cacheDir = File(workingDir, cacheDirName)
+ val baseDir = workingDir!!
+ val context = Context(networkName, backendType, backendOptions)
+ if (cacheDir.exists()) cacheDir.deleteRecursively()
+ val (containerPusher, instantiator, volume) = Backend.fromContext(context, cacheDir)
+ val nodeFinder = NodeFinder(baseDir)
+ val notaryFinder = NotaryFinder(baseDir)
+ val notaryCopier = NotaryCopier(cacheDir)
+
+ val nodeInstantiator = NodeInstantiator(instantiator, context)
+ val nodeBuilder = NodeBuilder()
+ val nodeCopier = NodeCopier(cacheDir)
+ val nodePusher = NodePusher(containerPusher, context)
+
+ val nodeDiscoveryFuture = CompletableFuture.supplyAsync {
+ val foundNodes = nodeFinder.findNodes()
+ .map { it to nodeCounts.getOrDefault(it.name.toLowerCase(), 1) }
+ .toMap()
+ foundNodes
+ }
+
+ val notaryDiscoveryFuture = CompletableFuture.supplyAsync {
+ val copiedNotaries = notaryFinder.findNotaries()
+ .map { foundNode: FoundNode ->
+ notaryCopier.copyNotary(foundNode)
+ }
+ volume.notariesForNetworkParams(copiedNotaries)
+ copiedNotaries
+ }
+
+ val notariesFuture = notaryDiscoveryFuture.thenCompose { copiedNotaries ->
+ copiedNotaries
+ .map { copiedNotary ->
+ nodeBuilder.buildNode(copiedNotary)
+ }.map { builtNotary ->
+ nodePusher.pushNode(builtNotary)
+ }.map { pushedNotary ->
+ pushedNotary.thenApplyAsync { nodeInstantiator.createInstanceRequest(it) }
+ }.map { instanceRequest ->
+ instanceRequest.thenComposeAsync { request ->
+ nodeInstantiator.instantiateNotaryInstance(request)
+ }
+ }.toSingleFuture()
+ }
+
+ val nodesFuture = notaryDiscoveryFuture.thenCombineAsync(nodeDiscoveryFuture) { _, nodeCount ->
+ nodeCount.keys
+ .map { foundNode ->
+ nodeCopier.copyNode(foundNode).let {
+ onNodeCopiedCallback.invoke(it)
+ it
+ }
+ }.map { copiedNode: CopiedNode ->
+ nodeBuilder.buildNode(copiedNode).let {
+ onNodeBuiltCallback.invoke(it)
+ it
+ }
+ }.map { builtNode ->
+ nodePusher.pushNode(builtNode).thenApplyAsync {
+ onNodePushedCallback.invoke(it)
+ it
+ }
+ }.map { pushedNode ->
+ pushedNode.thenApplyAsync {
+ nodeInstantiator.createInstanceRequests(it, nodeCount)
+ }
+ }.map { instanceRequests ->
+ instanceRequests.thenComposeAsync { requests ->
+ requests.map { request ->
+ nodeInstantiator.instantiateNodeInstance(request)
+ .thenApplyAsync { nodeInstance ->
+ context.registerNode(nodeInstance)
+ onNodeInstanceCallback.invoke(nodeInstance)
+ nodeInstance
+ }
+ }.toSingleFuture()
+ }
+ }.toSingleFuture()
+ }.thenCompose { it }.thenApplyAsync { it.flatten() }
+
+ return notariesFuture.thenCombineAsync(nodesFuture, { _, nodeInstances ->
+ context.networkInitiated = true
+ nodeInstances to context
+ })
+ }
+}
+
+fun List>.toSingleFuture(): CompletableFuture> {
+ return CompletableFuture.allOf(*this.toTypedArray()).thenApplyAsync {
+ this.map { it.getNow(null) }
+ }
+}
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/backends/AzureBackend.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/backends/AzureBackend.kt
new file mode 100644
index 0000000000..93a9890d46
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/backends/AzureBackend.kt
@@ -0,0 +1,63 @@
+package net.corda.bootstrapper.backends
+
+import com.microsoft.azure.CloudException
+import com.microsoft.azure.credentials.AzureCliCredentials
+import com.microsoft.azure.management.Azure
+import com.microsoft.rest.LogLevel
+import net.corda.bootstrapper.Constants
+import net.corda.bootstrapper.containers.instance.azure.AzureInstantiator
+import net.corda.bootstrapper.containers.push.azure.AzureContainerPusher
+import net.corda.bootstrapper.containers.push.azure.RegistryLocator
+import net.corda.bootstrapper.context.Context
+import net.corda.bootstrapper.volumes.azure.AzureSmbVolume
+import org.slf4j.LoggerFactory
+import java.util.concurrent.CompletableFuture
+
+data class AzureBackend(override val containerPusher: AzureContainerPusher,
+ override val instantiator: AzureInstantiator,
+ override val volume: AzureSmbVolume) : Backend {
+
+ companion object {
+
+ val LOG = LoggerFactory.getLogger(AzureBackend::class.java)
+
+ private val azure: Azure = kotlin.run {
+ Azure.configure()
+ .withLogLevel(LogLevel.NONE)
+ .authenticate(AzureCliCredentials.create())
+ .withDefaultSubscription()
+ }
+
+ fun fromContext(context: Context): AzureBackend {
+ val resourceGroupName = context.networkName.replace(Constants.ALPHA_NUMERIC_DOT_AND_UNDERSCORE_ONLY_REGEX, "")
+ val resourceGroup = try {
+ LOG.info("Attempting to find existing resourceGroup with name: $resourceGroupName")
+ val foundResourceGroup = azure.resourceGroups().getByName(resourceGroupName)
+
+ if (foundResourceGroup == null) {
+ LOG.info("No existing resourceGroup found creating new resourceGroup with name: $resourceGroupName")
+ azure.resourceGroups().define(resourceGroupName).withRegion(context.extraParams[Constants.REGION_ARG_NAME]).create()
+ } else {
+ LOG.info("Found existing resourceGroup, reusing")
+ foundResourceGroup
+ }
+ } catch (e: CloudException) {
+ throw RuntimeException(e)
+ }
+
+ val registryLocatorFuture = CompletableFuture.supplyAsync {
+ RegistryLocator(azure, resourceGroup)
+ }
+ val containerPusherFuture = registryLocatorFuture.thenApplyAsync {
+ AzureContainerPusher(azure, it.registry)
+ }
+ val azureNetworkStore = CompletableFuture.supplyAsync { AzureSmbVolume(azure, resourceGroup) }
+ val azureInstantiatorFuture = azureNetworkStore.thenCombine(registryLocatorFuture,
+ { azureVolume, registryLocator ->
+ AzureInstantiator(azure, registryLocator.registry, azureVolume, resourceGroup)
+ }
+ )
+ return AzureBackend(containerPusherFuture.get(), azureInstantiatorFuture.get(), azureNetworkStore.get())
+ }
+ }
+}
\ No newline at end of file
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/backends/Backend.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/backends/Backend.kt
new file mode 100644
index 0000000000..85209c1df5
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/backends/Backend.kt
@@ -0,0 +1,40 @@
+package net.corda.bootstrapper.backends
+
+import net.corda.bootstrapper.backends.Backend.BackendType.AZURE
+import net.corda.bootstrapper.backends.Backend.BackendType.LOCAL_DOCKER
+import net.corda.bootstrapper.containers.instance.Instantiator
+import net.corda.bootstrapper.containers.push.ContainerPusher
+import net.corda.bootstrapper.context.Context
+import net.corda.bootstrapper.volumes.Volume
+import java.io.File
+
+interface Backend {
+ companion object {
+ fun fromContext(context: Context, baseDir: File): Backend {
+ return when (context.backendType) {
+ AZURE -> AzureBackend.fromContext(context)
+ LOCAL_DOCKER -> DockerBackend.fromContext(context, baseDir)
+ }
+ }
+ }
+
+ val containerPusher: ContainerPusher
+ val instantiator: Instantiator
+ val volume: Volume
+
+ enum class BackendType {
+ AZURE, LOCAL_DOCKER
+ }
+
+ operator fun component1(): ContainerPusher {
+ return containerPusher
+ }
+
+ operator fun component2(): Instantiator {
+ return instantiator
+ }
+
+ operator fun component3(): Volume {
+ return volume
+ }
+}
\ No newline at end of file
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/backends/DockerBackend.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/backends/DockerBackend.kt
new file mode 100644
index 0000000000..fab1c3a24f
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/backends/DockerBackend.kt
@@ -0,0 +1,25 @@
+package net.corda.bootstrapper.backends
+
+import net.corda.bootstrapper.containers.instance.docker.DockerInstantiator
+import net.corda.bootstrapper.containers.push.docker.DockerContainerPusher
+import net.corda.bootstrapper.context.Context
+import net.corda.bootstrapper.volumes.docker.LocalVolume
+import java.io.File
+
+class DockerBackend(override val containerPusher: DockerContainerPusher,
+ override val instantiator: DockerInstantiator,
+ override val volume: LocalVolume) : Backend {
+
+
+ companion object {
+ fun fromContext(context: Context, baseDir: File): DockerBackend {
+ val dockerContainerPusher = DockerContainerPusher()
+ val localVolume = LocalVolume(baseDir, context)
+ val dockerInstantiator = DockerInstantiator(localVolume, context)
+ return DockerBackend(dockerContainerPusher, dockerInstantiator, localVolume)
+ }
+ }
+
+}
+
+
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/cli/CommandLineInterface.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/cli/CommandLineInterface.kt
new file mode 100644
index 0000000000..b7a27fe795
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/cli/CommandLineInterface.kt
@@ -0,0 +1,69 @@
+package net.corda.bootstrapper.cli
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import net.corda.bootstrapper.Constants
+import net.corda.bootstrapper.NetworkBuilder
+import net.corda.bootstrapper.backends.Backend
+import net.corda.bootstrapper.context.Context
+import net.corda.bootstrapper.nodes.NodeAdder
+import net.corda.bootstrapper.nodes.NodeInstantiator
+import net.corda.bootstrapper.toSingleFuture
+import net.corda.core.utilities.getOrThrow
+import java.io.File
+
+class CommandLineInterface {
+
+
+ fun run(parsedArgs: CliParser) {
+ val baseDir = parsedArgs.baseDirectory
+ val cacheDir = File(baseDir, Constants.BOOTSTRAPPER_DIR_NAME)
+ val networkName = parsedArgs.name
+ val objectMapper = Constants.getContextMapper()
+ val contextFile = File(cacheDir, "$networkName.yaml")
+ if (parsedArgs.isNew()) {
+ val (_, context) = NetworkBuilder.instance()
+ .withBasedir(baseDir)
+ .withNetworkName(networkName)
+ .withNodeCounts(parsedArgs.nodes)
+ .onNodeBuild { builtNode -> println("Built node: ${builtNode.name} to image: ${builtNode.localImageId}") }
+ .onNodePushed { pushedNode -> println("Pushed node: ${pushedNode.name} to: ${pushedNode.remoteImageName}") }
+ .onNodeInstance { instance ->
+ println("Instance of ${instance.name} with id: ${instance.nodeInstanceName} on address: " +
+ "${instance.reachableAddress} {ssh:${instance.portMapping[Constants.NODE_SSHD_PORT]}, " +
+ "p2p:${instance.portMapping[Constants.NODE_P2P_PORT]}}")
+ }
+ .withBackend(parsedArgs.backendType)
+ .withBackendOptions(parsedArgs.backendOptions())
+ .build().getOrThrow()
+ persistContext(contextFile, objectMapper, context)
+ } else {
+ val context = setupContextFromExisting(contextFile, objectMapper, networkName)
+ val (_, instantiator, _) = Backend.fromContext(context, cacheDir)
+ val nodeAdder = NodeAdder(context, NodeInstantiator(instantiator, context))
+ parsedArgs.nodesToAdd.map {
+ nodeAdder.addNode(context, Constants.ALPHA_NUMERIC_ONLY_REGEX.replace(it.toLowerCase(), ""))
+ }.toSingleFuture().getOrThrow()
+ persistContext(contextFile, objectMapper, context)
+ }
+
+ }
+
+ private fun setupContextFromExisting(contextFile: File, objectMapper: ObjectMapper, networkName: String): Context {
+ return contextFile.let {
+ if (it.exists()) {
+ it.inputStream().use {
+ objectMapper.readValue(it, Context::class.java)
+ }
+ } else {
+ throw IllegalStateException("No existing network context found")
+ }
+ }
+ }
+
+
+ private fun persistContext(contextFile: File, objectMapper: ObjectMapper, context: Context?) {
+ contextFile.outputStream().use {
+ objectMapper.writeValue(it, context)
+ }
+ }
+}
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/cli/CommandParsers.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/cli/CommandParsers.kt
new file mode 100644
index 0000000000..164f6ec092
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/cli/CommandParsers.kt
@@ -0,0 +1,68 @@
+package net.corda.bootstrapper.cli
+
+import com.microsoft.azure.management.resources.fluentcore.arm.Region
+import net.corda.bootstrapper.Constants
+import net.corda.bootstrapper.backends.Backend
+import picocli.CommandLine
+import picocli.CommandLine.Option
+import java.io.File
+
+open class GuiSwitch {
+
+ @Option(names = ["-h", "--help"], usageHelp = true, description = ["display this help message"])
+ var usageHelpRequested: Boolean = false
+
+ @Option(names = ["-g", "--gui"], description = ["Run in Gui Mode"])
+ var gui = false
+
+ @CommandLine.Unmatched
+ var unmatched = arrayListOf()
+}
+
+open class CliParser : GuiSwitch() {
+
+ @Option(names = ["-n", "--network-name"], description = ["The resource grouping to use"], required = true)
+ lateinit var name: String
+
+ @Option(names = ["-d", "--nodes-directory"], description = ["The directory to search for nodes in"])
+ var baseDirectory = File(System.getProperty("user.dir"))
+
+ @Option(names = ["-b", "--backend"], description = ["The backend to use when instantiating nodes"])
+ var backendType: Backend.BackendType = Backend.BackendType.LOCAL_DOCKER
+
+ @Option(names = ["-nodes"], split = ":", description = ["The number of each node to create NodeX:2 will create two instances of NodeX"])
+ var nodes: MutableMap = hashMapOf()
+
+ @Option(names = ["--add", "-a"])
+ var nodesToAdd: MutableList = arrayListOf()
+
+ fun isNew(): Boolean {
+ return nodesToAdd.isEmpty()
+ }
+
+ open fun backendOptions(): Map {
+ return emptyMap()
+ }
+
+}
+
+class AzureParser : CliParser() {
+
+ companion object {
+ val regions = Region.values().map { it.name() to it }.toMap()
+ }
+
+ @Option(names = ["-r", "--region"], description = ["The azure region to use"], converter = [RegionConverter::class])
+ var region: Region = Region.EUROPE_WEST
+
+ class RegionConverter : CommandLine.ITypeConverter {
+ override fun convert(value: String): Region {
+ return regions[value] ?: throw Error("Unknown azure region: $value")
+ }
+ }
+
+ override fun backendOptions(): Map {
+ return mapOf(Constants.REGION_ARG_NAME to region.name())
+ }
+
+}
\ No newline at end of file
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/containers/instance/InstanceInfo.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/containers/instance/InstanceInfo.kt
new file mode 100644
index 0000000000..0a9c863e0c
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/containers/instance/InstanceInfo.kt
@@ -0,0 +1,7 @@
+package net.corda.bootstrapper.containers.instance
+
+data class InstanceInfo(val groupId: String,
+ val instanceName: String,
+ val instanceAddress: String,
+ val reachableAddress: String,
+ val portMapping: Map = emptyMap())
\ No newline at end of file
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/containers/instance/Instantiator.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/containers/instance/Instantiator.kt
new file mode 100644
index 0000000000..1f7c963115
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/containers/instance/Instantiator.kt
@@ -0,0 +1,18 @@
+package net.corda.bootstrapper.containers.instance
+
+import java.util.concurrent.CompletableFuture
+
+
+interface Instantiator {
+ fun instantiateContainer(imageId: String,
+ portsToOpen: List,
+ instanceName: String,
+ env: Map? = null): CompletableFuture>>
+
+
+ companion object {
+ val ADDITIONAL_NODE_INFOS_PATH = "/opt/corda/additional-node-infos"
+ }
+
+ fun getExpectedFQDN(instanceName: String): String
+}
\ No newline at end of file
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/containers/instance/azure/AzureInstantiator.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/containers/instance/azure/AzureInstantiator.kt
new file mode 100644
index 0000000000..f537a5420a
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/containers/instance/azure/AzureInstantiator.kt
@@ -0,0 +1,85 @@
+package net.corda.bootstrapper.containers.instance.azure
+
+import com.microsoft.azure.management.Azure
+import com.microsoft.azure.management.containerinstance.ContainerGroup
+import com.microsoft.azure.management.containerinstance.ContainerGroupRestartPolicy
+import com.microsoft.azure.management.containerregistry.Registry
+import com.microsoft.azure.management.resources.ResourceGroup
+import com.microsoft.rest.ServiceCallback
+import net.corda.bootstrapper.Constants.Companion.restFriendlyName
+import net.corda.bootstrapper.containers.instance.Instantiator
+import net.corda.bootstrapper.containers.instance.Instantiator.Companion.ADDITIONAL_NODE_INFOS_PATH
+import net.corda.bootstrapper.containers.push.azure.RegistryLocator.Companion.parseCredentials
+import net.corda.bootstrapper.volumes.azure.AzureSmbVolume
+import org.slf4j.LoggerFactory
+import java.util.concurrent.CompletableFuture
+
+class AzureInstantiator(private val azure: Azure,
+ private val registry: Registry,
+ private val azureSmbVolume: AzureSmbVolume,
+ private val resourceGroup: ResourceGroup
+) : Instantiator {
+ override fun instantiateContainer(imageId: String,
+ portsToOpen: List,
+ instanceName: String,
+ env: Map?): CompletableFuture>> {
+
+ findAndKillExistingContainerGroup(resourceGroup, buildIdent(instanceName))
+
+ LOG.info("Starting instantiation of container: $instanceName using $imageId")
+ val registryAddress = registry.loginServerUrl()
+ val (username, password) = registry.parseCredentials();
+ val mountName = "node-setup"
+ val future = CompletableFuture>>().also {
+ azure.containerGroups().define(buildIdent(instanceName))
+ .withRegion(resourceGroup.region())
+ .withExistingResourceGroup(resourceGroup)
+ .withLinux()
+ .withPrivateImageRegistry(registryAddress, username, password)
+ .defineVolume(mountName)
+ .withExistingReadWriteAzureFileShare(azureSmbVolume.shareName)
+ .withStorageAccountName(azureSmbVolume.storageAccountName)
+ .withStorageAccountKey(azureSmbVolume.storageAccountKey)
+ .attach()
+ .defineContainerInstance(instanceName)
+ .withImage(imageId)
+ .withExternalTcpPorts(*portsToOpen.toIntArray())
+ .withVolumeMountSetting(mountName, ADDITIONAL_NODE_INFOS_PATH)
+ .withEnvironmentVariables(env ?: emptyMap())
+ .attach().withRestartPolicy(ContainerGroupRestartPolicy.ON_FAILURE)
+ .withDnsPrefix(buildIdent(instanceName))
+ .createAsync(object : ServiceCallback {
+ override fun failure(t: Throwable?) {
+ it.completeExceptionally(t)
+ }
+
+ override fun success(result: ContainerGroup) {
+ val fqdn = result.fqdn()
+ LOG.info("Completed instantiation: $instanceName is running at $fqdn with port(s) $portsToOpen exposed")
+ it.complete(result.fqdn() to portsToOpen.map { it to it }.toMap())
+ }
+ })
+ }
+ return future
+ }
+
+ private fun buildIdent(instanceName: String) = "$instanceName-${resourceGroup.restFriendlyName()}"
+
+ override fun getExpectedFQDN(instanceName: String): String {
+ return "${buildIdent(instanceName)}.${resourceGroup.region().name()}.azurecontainer.io"
+ }
+
+ fun findAndKillExistingContainerGroup(resourceGroup: ResourceGroup, containerName: String): ContainerGroup? {
+ val existingContainer = azure.containerGroups().getByResourceGroup(resourceGroup.name(), containerName)
+ if (existingContainer != null) {
+ LOG.info("Found an existing instance of: $containerName destroying ContainerGroup")
+ azure.containerGroups().deleteByResourceGroup(resourceGroup.name(), containerName)
+ }
+ return existingContainer;
+ }
+
+ companion object {
+ val LOG = LoggerFactory.getLogger(AzureInstantiator::class.java)
+ }
+
+}
\ No newline at end of file
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/containers/instance/docker/DockerInstantiator.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/containers/instance/docker/DockerInstantiator.kt
new file mode 100644
index 0000000000..0f29838407
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/containers/instance/docker/DockerInstantiator.kt
@@ -0,0 +1,99 @@
+package net.corda.bootstrapper.containers.instance.docker
+
+import com.github.dockerjava.api.model.*
+import net.corda.bootstrapper.Constants
+import net.corda.bootstrapper.containers.instance.Instantiator
+import net.corda.bootstrapper.context.Context
+import net.corda.bootstrapper.docker.DockerUtils
+import net.corda.bootstrapper.volumes.docker.LocalVolume
+import org.slf4j.LoggerFactory
+import java.util.concurrent.CompletableFuture
+
+
+class DockerInstantiator(private val volume: LocalVolume,
+ private val context: Context) : Instantiator {
+
+ val networkId = setupNetwork();
+
+ override fun instantiateContainer(imageId: String,
+ portsToOpen: List,
+ instanceName: String,
+ env: Map?): CompletableFuture>> {
+
+ val localClient = DockerUtils.createLocalDockerClient()
+ val convertedEnv = buildDockerEnv(env)
+ val nodeInfosVolume = Volume(Instantiator.ADDITIONAL_NODE_INFOS_PATH)
+ val existingContainers = localClient.listContainersCmd().withShowAll(true).exec()
+ .map { it.names.first() to it }
+ .filter { it.first.endsWith(instanceName) }
+ existingContainers.forEach { (_, container) ->
+ try {
+ localClient.killContainerCmd(container.id).exec()
+ LOG.info("Found running container: $instanceName killed")
+ } catch (e: Throwable) {
+ //container not running
+ }
+ try {
+ localClient.removeContainerCmd(container.id).exec()
+ LOG.info("Found existing container: $instanceName removed")
+ } catch (e: Throwable) {
+ //this *only* occurs of the container had been previously scheduled for removal
+ //but did not complete before this attempt was begun.
+ }
+
+ }
+ LOG.info("starting local docker instance of: $imageId with name $instanceName and env: $env")
+ val ports = (portsToOpen + Constants.NODE_RPC_ADMIN_PORT).map { ExposedPort.tcp(it) }.map { PortBinding(null, it) }.let { Ports(*it.toTypedArray()) }
+ val createCmd = localClient.createContainerCmd(imageId)
+ .withName(instanceName)
+ .withVolumes(nodeInfosVolume)
+ .withBinds(Bind(volume.getPath(), nodeInfosVolume))
+ .withPortBindings(ports)
+ .withExposedPorts(ports.bindings.map { it.key })
+ .withPublishAllPorts(true)
+ .withNetworkMode(networkId)
+ .withEnv(convertedEnv).exec()
+
+ localClient.startContainerCmd(createCmd.id).exec()
+ val foundContainer = localClient.listContainersCmd().exec()
+ .filter { it.id == (createCmd.id) }
+ .firstOrNull()
+
+ val portMappings = foundContainer?.ports?.map {
+ (it.privatePort ?: 0) to (it.publicPort ?: 0)
+ }?.toMap()?.toMap()
+ ?: portsToOpen.map { it to it }.toMap()
+
+
+
+ return CompletableFuture.completedFuture(("localhost") to portMappings)
+ }
+
+ private fun buildDockerEnv(env: Map?) =
+ (env ?: emptyMap()).entries.map { (key, value) -> "$key=$value" }.toList()
+
+ override fun getExpectedFQDN(instanceName: String): String {
+ return instanceName
+ }
+
+ private fun setupNetwork(): String {
+ val createLocalDockerClient = DockerUtils.createLocalDockerClient()
+ val existingNetworks = createLocalDockerClient.listNetworksCmd().withNameFilter(context.safeNetworkName).exec()
+ return if (existingNetworks.isNotEmpty()) {
+ if (existingNetworks.size > 1) {
+ throw IllegalStateException("Multiple local docker networks found with name ${context.safeNetworkName}")
+ } else {
+ LOG.info("Found existing network with name: ${context.safeNetworkName} reusing")
+ existingNetworks.first().id
+ }
+ } else {
+ val result = createLocalDockerClient.createNetworkCmd().withName(context.safeNetworkName).exec()
+ LOG.info("Created local docker network: ${result.id} with name: ${context.safeNetworkName}")
+ result.id
+ }
+ }
+
+ companion object {
+ val LOG = LoggerFactory.getLogger(DockerInstantiator::class.java)
+ }
+}
\ No newline at end of file
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/containers/push/ContainerPusher.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/containers/push/ContainerPusher.kt
new file mode 100644
index 0000000000..13714586ac
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/containers/push/ContainerPusher.kt
@@ -0,0 +1,9 @@
+package net.corda.bootstrapper.containers.push
+
+import java.util.concurrent.CompletableFuture
+
+interface ContainerPusher {
+ fun pushContainerToImageRepository(localImageId: String,
+ remoteImageName: String,
+ networkName: String): CompletableFuture
+}
\ No newline at end of file
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/containers/push/azure/AzureContainerPusher.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/containers/push/azure/AzureContainerPusher.kt
new file mode 100644
index 0000000000..2c57bb1a2e
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/containers/push/azure/AzureContainerPusher.kt
@@ -0,0 +1,62 @@
+package net.corda.bootstrapper.containers.push.azure
+
+import com.github.dockerjava.api.async.ResultCallback
+import com.github.dockerjava.api.model.PushResponseItem
+import com.microsoft.azure.management.Azure
+import com.microsoft.azure.management.containerregistry.Registry
+import net.corda.bootstrapper.containers.push.ContainerPusher
+import net.corda.bootstrapper.containers.push.azure.RegistryLocator.Companion.parseCredentials
+import net.corda.bootstrapper.docker.DockerUtils
+import org.slf4j.LoggerFactory
+import java.io.Closeable
+import java.util.concurrent.CompletableFuture
+
+
+class AzureContainerPusher(private val azure: Azure, private val azureRegistry: Registry) : ContainerPusher {
+
+
+ override fun pushContainerToImageRepository(localImageId: String,
+ remoteImageName: String,
+ networkName: String): CompletableFuture {
+
+
+ val (registryUser, registryPassword) = azureRegistry.parseCredentials()
+ val dockerClient = DockerUtils.createDockerClient(
+ azureRegistry.loginServerUrl(),
+ registryUser,
+ registryPassword)
+
+ val privateRepoUrl = "${azureRegistry.loginServerUrl()}/$remoteImageName".toLowerCase()
+ dockerClient.tagImageCmd(localImageId, privateRepoUrl, networkName).exec()
+ val result = CompletableFuture()
+ dockerClient.pushImageCmd("$privateRepoUrl:$networkName")
+ .withAuthConfig(dockerClient.authConfig())
+ .exec(object : ResultCallback {
+ override fun onComplete() {
+ LOG.info("completed PUSH image: $localImageId to registryURL: $privateRepoUrl:$networkName")
+ result.complete("$privateRepoUrl:$networkName")
+ }
+
+ override fun close() {
+ }
+
+ override fun onNext(`object`: PushResponseItem) {
+ }
+
+ override fun onError(throwable: Throwable?) {
+ result.completeExceptionally(throwable)
+ }
+
+ override fun onStart(closeable: Closeable?) {
+ LOG.info("starting PUSH image: $localImageId to registryURL: $privateRepoUrl:$networkName")
+ }
+ })
+ return result
+ }
+
+ companion object {
+ val LOG = LoggerFactory.getLogger(AzureContainerPusher::class.java)
+ }
+
+}
+
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/containers/push/azure/AzureRegistryLocator.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/containers/push/azure/AzureRegistryLocator.kt
new file mode 100644
index 0000000000..f9ee36c259
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/containers/push/azure/AzureRegistryLocator.kt
@@ -0,0 +1,55 @@
+package net.corda.bootstrapper.containers.push.azure
+
+import com.microsoft.azure.management.Azure
+import com.microsoft.azure.management.containerregistry.AccessKeyType
+import com.microsoft.azure.management.containerregistry.Registry
+import com.microsoft.azure.management.resources.ResourceGroup
+import net.corda.bootstrapper.Constants.Companion.restFriendlyName
+import net.corda.bootstrapper.containers.instance.azure.AzureInstantiator
+import org.slf4j.LoggerFactory
+
+class RegistryLocator(private val azure: Azure,
+ private val resourceGroup: ResourceGroup) {
+
+
+ val registry: Registry = locateRegistry()
+
+
+ private fun locateRegistry(): Registry {
+ LOG.info("Attempting to find existing registry with name: ${resourceGroup.restFriendlyName()}")
+ val found = azure.containerRegistries().getByResourceGroup(resourceGroup.name(), resourceGroup.restFriendlyName())
+
+ if (found == null) {
+ LOG.info("Did not find existing container registry - creating new registry with name ${resourceGroup.restFriendlyName()}")
+ return azure.containerRegistries()
+ .define(resourceGroup.restFriendlyName())
+ .withRegion(resourceGroup.region().name())
+ .withExistingResourceGroup(resourceGroup)
+ .withBasicSku()
+ .withRegistryNameAsAdminUser()
+ .create()
+
+ } else {
+ LOG.info("found existing registry with name: ${resourceGroup.restFriendlyName()} reusing")
+ return found
+ }
+ }
+
+ companion object {
+ fun Registry.parseCredentials(): Pair {
+ val credentials = this.credentials
+ return credentials.username() to
+ (credentials.accessKeys()[AccessKeyType.PRIMARY]
+ ?: throw IllegalStateException("no registry password found"))
+ }
+
+ val LOG = LoggerFactory.getLogger(AzureInstantiator::class.java)
+
+ }
+
+
+}
+
+
+
+
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/containers/push/docker/DockerContainerPusher.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/containers/push/docker/DockerContainerPusher.kt
new file mode 100644
index 0000000000..8c53e6edae
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/containers/push/docker/DockerContainerPusher.kt
@@ -0,0 +1,15 @@
+package net.corda.bootstrapper.containers.push.docker
+
+import net.corda.bootstrapper.containers.push.ContainerPusher
+import net.corda.bootstrapper.docker.DockerUtils
+import java.util.concurrent.CompletableFuture
+
+class DockerContainerPusher : ContainerPusher {
+
+
+ override fun pushContainerToImageRepository(localImageId: String, remoteImageName: String, networkName: String): CompletableFuture {
+ val dockerClient = DockerUtils.createLocalDockerClient()
+ dockerClient.tagImageCmd(localImageId, remoteImageName, networkName).withForce().exec()
+ return CompletableFuture.completedFuture("$remoteImageName:$networkName")
+ }
+}
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/context/Context.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/context/Context.kt
new file mode 100644
index 0000000000..8b72714c8d
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/context/Context.kt
@@ -0,0 +1,69 @@
+package net.corda.bootstrapper.context
+
+import net.corda.bootstrapper.Constants
+import net.corda.bootstrapper.backends.Backend
+import net.corda.bootstrapper.nodes.NodeInstanceRequest
+import net.corda.core.identity.CordaX500Name
+import org.apache.activemq.artemis.utils.collections.ConcurrentHashSet
+import java.util.concurrent.ConcurrentHashMap
+
+class Context(val networkName: String, val backendType: Backend.BackendType, backendOptions: Map = emptyMap()) {
+
+
+ @Volatile
+ var safeNetworkName: String = networkName.replace(Constants.ALPHA_NUMERIC_ONLY_REGEX, "").toLowerCase()
+
+ @Volatile
+ var nodes: MutableMap> = ConcurrentHashMap()
+
+ @Volatile
+ var networkInitiated: Boolean = false
+
+ @Volatile
+ var extraParams = ConcurrentHashMap(backendOptions)
+
+ private fun registerNode(name: String, nodeInstanceRequest: NodeInstanceRequest) {
+ nodes.computeIfAbsent(name, { _ -> ConcurrentHashSet() }).add(nodeInstanceRequest.toPersistable())
+ }
+
+ fun registerNode(request: NodeInstanceRequest) {
+ registerNode(request.name, request)
+ }
+
+
+ data class PersistableNodeInstance(
+ val groupName: String,
+ val groupX500: CordaX500Name?,
+ val instanceName: String,
+ val instanceX500: String,
+ val localImageId: String?,
+ val remoteImageName: String,
+ val rpcPort: Int?,
+ val fqdn: String,
+ val rpcUser: String,
+ val rpcPassword: String)
+
+
+ companion object {
+ fun fromInstanceRequest(nodeInstanceRequest: NodeInstanceRequest): PersistableNodeInstance {
+ return PersistableNodeInstance(
+ nodeInstanceRequest.name,
+ nodeInstanceRequest.nodeConfig.myLegalName,
+ nodeInstanceRequest.nodeInstanceName,
+ nodeInstanceRequest.actualX500,
+ nodeInstanceRequest.localImageId,
+ nodeInstanceRequest.remoteImageName,
+ nodeInstanceRequest.nodeConfig.rpcOptions.address!!.port,
+ nodeInstanceRequest.expectedFqName,
+ "",
+ ""
+ )
+
+ }
+ }
+
+ fun NodeInstanceRequest.toPersistable(): PersistableNodeInstance {
+ return fromInstanceRequest(this)
+ }
+}
+
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/docker/DockerUtils.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/docker/DockerUtils.kt
new file mode 100644
index 0000000000..f4c5680b75
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/docker/DockerUtils.kt
@@ -0,0 +1,35 @@
+package net.corda.bootstrapper.docker
+
+import com.github.dockerjava.api.DockerClient
+import com.github.dockerjava.core.DefaultDockerClientConfig
+import com.github.dockerjava.core.DockerClientBuilder
+import com.github.dockerjava.core.DockerClientConfig
+import org.apache.commons.lang3.SystemUtils
+
+object DockerUtils {
+
+ @Throws(Exception::class)
+ fun createDockerClient(registryServerUrl: String, username: String, password: String): DockerClient {
+ return DockerClientBuilder.getInstance(createDockerClientConfig(registryServerUrl, username, password))
+ .build()
+ }
+
+ fun createLocalDockerClient(): DockerClient {
+ return if (SystemUtils.IS_OS_WINDOWS) {
+ DockerClientBuilder.getInstance("tcp://127.0.0.1:2375").build()
+ } else {
+ DockerClientBuilder.getInstance().build()
+ }
+ }
+
+ private fun createDockerClientConfig(registryServerUrl: String, username: String, password: String): DockerClientConfig {
+ return DefaultDockerClientConfig.createDefaultConfigBuilder()
+ .withDockerTlsVerify(false)
+ .withRegistryUrl(registryServerUrl)
+ .withRegistryUsername(username)
+ .withRegistryPassword(password)
+ .build()
+ }
+
+
+}
\ No newline at end of file
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/gui/BootstrapperView.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/gui/BootstrapperView.kt
new file mode 100644
index 0000000000..32be5574eb
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/gui/BootstrapperView.kt
@@ -0,0 +1,323 @@
+package net.corda.bootstrapper.gui
+
+import com.microsoft.azure.management.resources.fluentcore.arm.Region
+import javafx.beans.property.SimpleObjectProperty
+import javafx.collections.transformation.SortedList
+import javafx.event.EventHandler
+import javafx.scene.control.ChoiceDialog
+import javafx.scene.control.TableView.CONSTRAINED_RESIZE_POLICY
+import javafx.scene.control.TextInputDialog
+import javafx.scene.input.MouseEvent
+import javafx.scene.layout.Priority
+import javafx.stage.DirectoryChooser
+import net.corda.bootstrapper.Constants
+import net.corda.bootstrapper.GuiUtils
+import net.corda.bootstrapper.NetworkBuilder
+import net.corda.bootstrapper.backends.Backend
+import net.corda.bootstrapper.context.Context
+import net.corda.bootstrapper.nodes.*
+import net.corda.bootstrapper.notaries.NotaryFinder
+import org.apache.commons.lang3.RandomStringUtils
+import tornadofx.*
+import java.io.File
+import java.util.*
+import java.util.concurrent.CompletableFuture
+import kotlin.collections.ArrayList
+
+class BootstrapperView : View("Network Bootstrapper") {
+
+ val YAML_MAPPER = Constants.getContextMapper()
+
+
+ val controller: State by inject()
+
+ val textarea = textarea {
+ maxWidth = Double.MAX_VALUE
+ maxHeight = Double.MAX_VALUE
+ }
+
+ override val root = vbox {
+
+ menubar {
+ menu("File") {
+ item("Open") {
+ action {
+ selectNodeDirectory().thenAcceptAsync({ (notaries: List, nodes: List) ->
+ controller.nodes(nodes)
+ controller.notaries(notaries)
+ })
+ }
+ }
+
+ item("Build") {
+ enableWhen(controller.baseDir.isNotNull)
+ action {
+ controller.clear()
+ val availableBackends = getAvailableBackends()
+ val backend = ChoiceDialog(availableBackends.first(), availableBackends).showAndWait()
+ var networkName = "gui-network"
+ backend.ifPresent { selectedBackEnd ->
+
+ val backendParams = when (selectedBackEnd) {
+ Backend.BackendType.LOCAL_DOCKER -> {
+
+ emptyMap()
+ }
+ Backend.BackendType.AZURE -> {
+ val defaultName = RandomStringUtils.randomAlphabetic(4) + "-network"
+ val textInputDialog = TextInputDialog(defaultName)
+ textInputDialog.title = "Choose Network Name"
+ networkName = textInputDialog.showAndWait().orElseGet { defaultName }
+ mapOf(Constants.REGION_ARG_NAME to ChoiceDialog(Region.EUROPE_WEST, Region.values().toList().sortedBy { it.name() }).showAndWait().get().name())
+ }
+ }
+
+ val nodeCount = controller.foundNodes.map { it.id to it.count }.toMap()
+ val result = NetworkBuilder.instance()
+ .withBasedir(controller.baseDir.get())
+ .withNetworkName(networkName)
+ .onNodeBuild(controller::addBuiltNode)
+ .onNodePushed(controller::addPushedNode)
+ .onNodeInstance(controller::addInstance)
+ .withBackend(selectedBackEnd)
+ .withNodeCounts(nodeCount)
+ .withBackendOptions(backendParams)
+ .build()
+ result.handle { v, t ->
+ runLater {
+ if (t != null) {
+ GuiUtils.showException("Failed to build network", "Failure due to", t)
+ } else {
+ controller.networkContext.set(v.second)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ item("Add Node") {
+ enableWhen(controller.networkContext.isNotNull)
+ action {
+ val foundNodes = controller.foundNodes.map { it.id }
+ val nodeToAdd = ChoiceDialog(foundNodes.first(), *foundNodes.toTypedArray()).showAndWait()
+ val context = controller.networkContext.value
+ nodeToAdd.ifPresent { node ->
+ runLater {
+ val (_, instantiator, _) = Backend.fromContext(
+ context,
+ File(controller.baseDir.get(), Constants.BOOTSTRAPPER_DIR_NAME))
+ val nodeAdder = NodeAdder(context, NodeInstantiator(instantiator, context))
+ nodeAdder.addNode(context, node).handleAsync { instanceInfo, t ->
+ t?.let {
+ GuiUtils.showException("Failed", "Failed to add node", it)
+ }
+ instanceInfo?.let {
+ runLater {
+ controller.addInstance(NodeInstanceTableEntry(
+ it.groupId,
+ it.instanceName,
+ it.instanceAddress,
+ it.reachableAddress,
+ it.portMapping[Constants.NODE_P2P_PORT] ?: Constants.NODE_P2P_PORT,
+ it.portMapping[Constants.NODE_SSHD_PORT]
+ ?: Constants.NODE_SSHD_PORT))
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ hbox {
+ vbox {
+ label("Nodes to build")
+ val foundNodesTable = tableview(controller.foundNodes) {
+ readonlyColumn("ID", FoundNodeTableEntry::id)
+ column("Count", FoundNodeTableEntry::count).makeEditable()
+ vgrow = Priority.ALWAYS
+ hgrow = Priority.ALWAYS
+ }
+ foundNodesTable.columnResizePolicy = CONSTRAINED_RESIZE_POLICY
+ label("Notaries to build")
+ val notaryListView = listview(controller.foundNotaries) {
+ vgrow = Priority.ALWAYS
+ hgrow = Priority.ALWAYS
+ }
+ notaryListView.cellFormat { text = it.name }
+ vgrow = Priority.ALWAYS
+ hgrow = Priority.ALWAYS
+ }
+
+ vbox {
+
+ label("Built Nodes")
+ tableview(controller.builtNodes) {
+ readonlyColumn("ID", BuiltNodeTableEntry::id)
+ readonlyColumn("LocalImageId", BuiltNodeTableEntry::localImageId)
+ columnResizePolicy = CONSTRAINED_RESIZE_POLICY
+ vgrow = Priority.ALWAYS
+ hgrow = Priority.ALWAYS
+ }
+
+
+ label("Pushed Nodes")
+ tableview(controller.pushedNodes) {
+ readonlyColumn("ID", PushedNode::name)
+ readonlyColumn("RemoteImageId", PushedNode::remoteImageName)
+ columnResizePolicy = CONSTRAINED_RESIZE_POLICY
+ vgrow = Priority.ALWAYS
+ hgrow = Priority.ALWAYS
+ }
+ vgrow = Priority.ALWAYS
+ hgrow = Priority.ALWAYS
+ }
+
+ borderpane {
+ top = vbox {
+ label("Instances")
+ tableview(controller.nodeInstances) {
+ onMouseClicked = EventHandler { _ ->
+ textarea.text = YAML_MAPPER.writeValueAsString(selectionModel.selectedItem)
+ }
+ readonlyColumn("ID", NodeInstanceTableEntry::id)
+ readonlyColumn("InstanceId", NodeInstanceTableEntry::nodeInstanceName)
+ readonlyColumn("Address", NodeInstanceTableEntry::address)
+ columnResizePolicy = CONSTRAINED_RESIZE_POLICY
+ }
+ }
+ center = textarea
+ vgrow = Priority.ALWAYS
+ hgrow = Priority.ALWAYS
+ }
+
+ vgrow = Priority.ALWAYS
+ hgrow = Priority.ALWAYS
+ }
+
+ }
+
+ private fun getAvailableBackends(): List {
+ return Backend.BackendType.values().toMutableList();
+ }
+
+
+ fun selectNodeDirectory(): CompletableFuture, List>> {
+ val fileChooser = DirectoryChooser();
+ fileChooser.initialDirectory = File(System.getProperty("user.home"))
+ val file = fileChooser.showDialog(null)
+ controller.baseDir.set(file)
+ return processSelectedDirectory(file)
+ }
+
+
+ fun processSelectedDirectory(dir: File): CompletableFuture, List>> {
+ val foundNodes = CompletableFuture.supplyAsync {
+ val nodeFinder = NodeFinder(dir)
+ nodeFinder.findNodes()
+ }
+ val foundNotaries = CompletableFuture.supplyAsync {
+ val notaryFinder = NotaryFinder(dir)
+ notaryFinder.findNotaries()
+ }
+ return foundNodes.thenCombine(foundNotaries) { nodes, notaries ->
+ notaries to nodes
+ }
+ }
+}
+
+class State : Controller() {
+
+ val foundNodes = Collections.synchronizedList(ArrayList()).observable()
+ val builtNodes = Collections.synchronizedList(ArrayList()).observable()
+ val pushedNodes = Collections.synchronizedList(ArrayList()).observable()
+
+ private val backingUnsortedInstances = Collections.synchronizedList(ArrayList()).observable()
+ val nodeInstances = SortedList(backingUnsortedInstances, COMPARATOR)
+
+ val foundNotaries = Collections.synchronizedList(ArrayList()).observable()
+ val networkContext = SimpleObjectProperty(null)
+
+ fun clear() {
+ builtNodes.clear()
+ pushedNodes.clear()
+ backingUnsortedInstances.clear()
+ networkContext.set(null)
+ }
+
+ fun nodes(nodes: List) {
+ foundNodes.clear()
+ nodes.forEach { addFoundNode(it) }
+ }
+
+ fun notaries(notaries: List) {
+ foundNotaries.clear()
+ notaries.forEach { runLater { foundNotaries.add(it) } }
+ }
+
+ var baseDir = SimpleObjectProperty(null)
+
+
+ fun addFoundNode(foundNode: FoundNode) {
+ runLater {
+ foundNodes.add(FoundNodeTableEntry(foundNode.name))
+ }
+ }
+
+ fun addBuiltNode(builtNode: BuiltNode) {
+ runLater {
+ builtNodes.add(BuiltNodeTableEntry(builtNode.name, builtNode.localImageId))
+ }
+ }
+
+ fun addPushedNode(pushedNode: PushedNode) {
+ runLater {
+ pushedNodes.add(pushedNode)
+ }
+ }
+
+ fun addInstance(nodeInstance: NodeInstance) {
+ runLater {
+ backingUnsortedInstances.add(NodeInstanceTableEntry(
+ nodeInstance.name,
+ nodeInstance.nodeInstanceName,
+ nodeInstance.expectedFqName,
+ nodeInstance.reachableAddress,
+ nodeInstance.portMapping[Constants.NODE_P2P_PORT] ?: Constants.NODE_P2P_PORT,
+ nodeInstance.portMapping[Constants.NODE_SSHD_PORT] ?: Constants.NODE_SSHD_PORT))
+ }
+ }
+
+ fun addInstance(nodeInstance: NodeInstanceTableEntry) {
+ runLater {
+ backingUnsortedInstances.add(nodeInstance)
+ }
+ }
+
+ companion object {
+ val COMPARATOR: (NodeInstanceTableEntry, NodeInstanceTableEntry) -> Int = { o1, o2 ->
+ if (o1.id == (o2.id)) {
+ o1.nodeInstanceName.compareTo(o2.nodeInstanceName)
+ } else {
+ o1.id.compareTo(o2.id)
+ }
+ }
+ }
+
+
+}
+
+data class FoundNodeTableEntry(val id: String,
+ @Volatile var count: Int = 1)
+
+data class BuiltNodeTableEntry(val id: String, val localImageId: String)
+
+data class NodeInstanceTableEntry(val id: String,
+ val nodeInstanceName: String,
+ val address: String,
+ val locallyReachableAddress: String,
+ val rpcPort: Int,
+ val sshPort: Int)
\ No newline at end of file
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/gui/Gui.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/gui/Gui.kt
new file mode 100644
index 0000000000..cc71845369
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/gui/Gui.kt
@@ -0,0 +1,11 @@
+package net.corda.bootstrapper.gui
+
+import javafx.application.Application
+import tornadofx.App
+
+class Gui : App(BootstrapperView::class) {
+ companion object {
+ @JvmStatic
+ fun main(args: Array) = Application.launch(Gui::class.java, *args)
+ }
+}
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/BuiltNode.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/BuiltNode.kt
new file mode 100644
index 0000000000..47515f9dce
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/BuiltNode.kt
@@ -0,0 +1,22 @@
+package net.corda.bootstrapper.nodes
+
+import net.corda.node.services.config.NodeConfiguration
+import java.io.File
+
+open class BuiltNode(configFile: File, baseDirectory: File,
+ copiedNodeConfig: File, copiedNodeDir: File,
+ val nodeConfig: NodeConfiguration, val localImageId: String) : CopiedNode(configFile, baseDirectory, copiedNodeConfig, copiedNodeDir) {
+
+
+ override fun toString(): String {
+ return "BuiltNode(" +
+ "nodeConfig=$nodeConfig," +
+ "localImageId='$localImageId'" +
+ ")" +
+ " ${super.toString()}"
+ }
+
+ fun toPushedNode(remoteImageName: String): PushedNode {
+ return PushedNode(configFile, baseDirectory, copiedNodeConfig, copiedNodeDir, nodeConfig, localImageId, remoteImageName)
+ }
+}
\ No newline at end of file
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/CopiedNode.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/CopiedNode.kt
new file mode 100644
index 0000000000..6e6c04c67f
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/CopiedNode.kt
@@ -0,0 +1,40 @@
+package net.corda.bootstrapper.nodes
+
+import net.corda.node.services.config.NodeConfiguration
+import java.io.File
+
+open class CopiedNode(configFile: File, baseDirectory: File,
+ open val copiedNodeConfig: File, open val copiedNodeDir: File) :
+ FoundNode(configFile, baseDirectory) {
+
+ constructor(foundNode: FoundNode, copiedNodeConfig: File, copiedNodeDir: File) : this(
+ foundNode.configFile, foundNode.baseDirectory, copiedNodeConfig, copiedNodeDir
+ )
+
+ operator fun component4(): File {
+ return copiedNodeDir;
+ }
+
+ operator fun component5(): File {
+ return copiedNodeConfig;
+ }
+
+
+ fun builtNode(nodeConfig: NodeConfiguration, imageId: String): BuiltNode {
+ return BuiltNode(configFile, baseDirectory, copiedNodeConfig, copiedNodeDir, nodeConfig, imageId)
+ }
+
+ override fun toString(): String {
+ return "CopiedNode(" +
+ "copiedNodeConfig=$copiedNodeConfig," +
+ "copiedNodeDir=$copiedNodeDir" +
+ ")" +
+ " ${super.toString()}"
+ }
+
+ fun toBuiltNode(nodeConfig: NodeConfiguration, localImageId: String): BuiltNode {
+ return BuiltNode(this.configFile, this.baseDirectory, this.copiedNodeConfig, this.copiedNodeDir, nodeConfig, localImageId)
+ }
+
+
+}
\ No newline at end of file
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/FoundNode.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/FoundNode.kt
new file mode 100644
index 0000000000..477cbd1b75
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/FoundNode.kt
@@ -0,0 +1,53 @@
+package net.corda.bootstrapper.nodes
+
+import java.io.File
+
+open class FoundNode(open val configFile: File,
+ open val baseDirectory: File = configFile.parentFile,
+ val name: String = configFile.parentFile.name.toLowerCase().replace(net.corda.bootstrapper.Constants.ALPHA_NUMERIC_ONLY_REGEX, "")) {
+
+
+ operator fun component1(): File {
+ return baseDirectory;
+ }
+
+ operator fun component2(): File {
+ return configFile;
+ }
+
+ operator fun component3(): String {
+ return name;
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as FoundNode
+
+ if (configFile != other.configFile) return false
+ if (baseDirectory != other.baseDirectory) return false
+ if (name != other.name) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = configFile.hashCode()
+ result = 31 * result + baseDirectory.hashCode()
+ result = 31 * result + name.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "FoundNode(name='$name', configFile=$configFile, baseDirectory=$baseDirectory)"
+ }
+
+ fun toCopiedNode(copiedNodeConfig: File, copiedNodeDir: File): CopiedNode {
+ return CopiedNode(this.configFile, this.baseDirectory, copiedNodeConfig, copiedNodeDir)
+ }
+
+
+}
+
+
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/NodeAdder.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/NodeAdder.kt
new file mode 100644
index 0000000000..4de0d53ebe
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/NodeAdder.kt
@@ -0,0 +1,26 @@
+package net.corda.bootstrapper.nodes
+
+import net.corda.bootstrapper.containers.instance.InstanceInfo
+import net.corda.bootstrapper.context.Context
+import java.util.concurrent.CompletableFuture
+
+class NodeAdder(val context: Context,
+ val nodeInstantiator: NodeInstantiator) {
+
+ fun addNode(context: Context, nodeGroupName: String): CompletableFuture {
+ return synchronized(context) {
+ val nodeGroup = context.nodes[nodeGroupName]!!
+ val nodeInfo = nodeGroup.iterator().next()
+ val currentNodeSize = nodeGroup.size
+ val newInstanceX500 = nodeInfo.groupX500!!.copy(commonName = nodeInfo.groupX500.commonName + (currentNodeSize)).toString()
+ val newInstanceName = nodeGroupName + (currentNodeSize)
+ val nextNodeInfo = nodeInfo.copy(
+ instanceX500 = newInstanceX500,
+ instanceName = newInstanceName,
+ fqdn = nodeInstantiator.expectedFqdn(newInstanceName)
+ )
+ nodeGroup.add(nextNodeInfo)
+ nodeInstantiator.instantiateNodeInstance(nextNodeInfo)
+ }
+ }
+}
\ No newline at end of file
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/NodeBuilder.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/NodeBuilder.kt
new file mode 100644
index 0000000000..9b55321cdd
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/NodeBuilder.kt
@@ -0,0 +1,48 @@
+package net.corda.bootstrapper.nodes
+
+import com.github.dockerjava.core.command.BuildImageResultCallback
+import com.typesafe.config.Config
+import com.typesafe.config.ConfigFactory
+import com.typesafe.config.ConfigValueFactory
+import net.corda.bootstrapper.docker.DockerUtils
+import net.corda.node.services.config.NodeConfiguration
+import net.corda.node.services.config.parseAsNodeConfiguration
+import org.slf4j.LoggerFactory
+import java.io.File
+
+open class NodeBuilder {
+
+ companion object {
+ val LOG = LoggerFactory.getLogger(NodeBuilder::class.java)
+ }
+
+ fun buildNode(copiedNode: CopiedNode): BuiltNode {
+ val localDockerClient = DockerUtils.createLocalDockerClient()
+ val copiedNodeConfig = copiedNode.copiedNodeConfig
+ val nodeDir = copiedNodeConfig.parentFile
+ if (!copiedNodeConfig.exists()) {
+ throw IllegalStateException("There is no nodeConfig for dir: " + copiedNodeConfig)
+ }
+ val nodeConfig = ConfigFactory.parseFile(copiedNodeConfig)
+ LOG.info("starting to build docker image for: " + nodeDir)
+ val nodeImageId = localDockerClient.buildImageCmd()
+ .withDockerfile(File(nodeDir, "Dockerfile"))
+ .withBaseDirectory(nodeDir)
+ .exec(BuildImageResultCallback()).awaitImageId()
+ LOG.info("finished building docker image for: $nodeDir with id: $nodeImageId")
+ val config = nodeConfig.parseAsNodeConfigWithFallback(ConfigFactory.parseFile(copiedNode.configFile))
+ return copiedNode.builtNode(config, nodeImageId)
+ }
+
+}
+
+
+fun Config.parseAsNodeConfigWithFallback(preCopyConfig: Config): NodeConfiguration {
+ val nodeConfig = this
+ .withValue("baseDirectory", ConfigValueFactory.fromAnyRef(""))
+ .withFallback(ConfigFactory.parseResources("reference.conf"))
+ .withFallback(preCopyConfig)
+ .resolve()
+ return nodeConfig.parseAsNodeConfiguration()
+}
+
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/NodeCopier.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/NodeCopier.kt
new file mode 100644
index 0000000000..fa58d90b89
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/NodeCopier.kt
@@ -0,0 +1,101 @@
+package net.corda.bootstrapper.nodes
+
+import com.typesafe.config.ConfigFactory
+import com.typesafe.config.ConfigRenderOptions
+import com.typesafe.config.ConfigValue
+import org.slf4j.LoggerFactory
+import java.io.File
+
+open class NodeCopier(private val cacheDir: File) {
+
+
+ fun copyNode(foundNode: FoundNode): CopiedNode {
+ val nodeCacheDir = File(cacheDir, foundNode.baseDirectory.name)
+ nodeCacheDir.deleteRecursively()
+ LOG.info("copying: ${foundNode.baseDirectory} to $nodeCacheDir")
+ foundNode.baseDirectory.copyRecursively(nodeCacheDir, overwrite = true)
+ copyBootstrapperFiles(nodeCacheDir)
+ val configInCacheDir = File(nodeCacheDir, "node.conf")
+ LOG.info("Applying precanned config " + configInCacheDir)
+ val rpcSettings = getDefaultRpcSettings()
+ val sshSettings = getDefaultSshSettings();
+ mergeConfigs(configInCacheDir, rpcSettings, sshSettings)
+ return CopiedNode(foundNode, configInCacheDir, nodeCacheDir)
+ }
+
+
+ fun copyBootstrapperFiles(nodeCacheDir: File) {
+ this.javaClass.classLoader.getResourceAsStream("node-Dockerfile").use { nodeDockerFileInStream ->
+ val nodeDockerFile = File(nodeCacheDir, "Dockerfile")
+ nodeDockerFile.outputStream().use { nodeDockerFileOutStream ->
+ nodeDockerFileInStream.copyTo(nodeDockerFileOutStream)
+ }
+ }
+
+ this.javaClass.classLoader.getResourceAsStream("run-corda-node.sh").use { nodeRunScriptInStream ->
+ val nodeRunScriptFile = File(nodeCacheDir, "run-corda.sh")
+ nodeRunScriptFile.outputStream().use { nodeDockerFileOutStream ->
+ nodeRunScriptInStream.copyTo(nodeDockerFileOutStream)
+ }
+ }
+
+ this.javaClass.classLoader.getResourceAsStream("node_info_watcher.sh").use { nodeRunScriptInStream ->
+ val nodeInfoWatcherFile = File(nodeCacheDir, "node_info_watcher.sh")
+ nodeInfoWatcherFile.outputStream().use { nodeDockerFileOutStream ->
+ nodeRunScriptInStream.copyTo(nodeDockerFileOutStream)
+ }
+ }
+ }
+
+ internal fun getDefaultRpcSettings(): ConfigValue {
+ return javaClass
+ .classLoader
+ .getResourceAsStream("rpc-settings.conf")
+ .reader().use {
+ ConfigFactory.parseReader(it)
+ }.getValue("rpcSettings")
+ }
+
+ internal fun getDefaultSshSettings(): ConfigValue {
+ return javaClass
+ .classLoader
+ .getResourceAsStream("ssh.conf")
+ .reader().use {
+ ConfigFactory.parseReader(it)
+ }.getValue("sshd")
+ }
+
+ internal fun mergeConfigs(configInCacheDir: File,
+ rpcSettings: ConfigValue,
+ sshSettings: ConfigValue,
+ mergeMode: Mode = Mode.NODE) {
+ var trimmedConfig = ConfigFactory.parseFile(configInCacheDir)
+ .withoutPath("compatibilityZoneURL")
+ .withValue("rpcSettings", rpcSettings)
+ .withValue("sshd", sshSettings)
+
+ if (mergeMode == Mode.NODE) {
+ trimmedConfig = trimmedConfig.withoutPath("p2pAddress")
+ }
+
+ configInCacheDir.outputStream().use {
+ trimmedConfig.root().render(ConfigRenderOptions
+ .defaults()
+ .setOriginComments(false)
+ .setComments(false)
+ .setFormatted(true)
+ .setJson(false)).byteInputStream().copyTo(it)
+ }
+ }
+
+
+ internal enum class Mode {
+ NOTARY, NODE
+ }
+
+
+ companion object {
+ val LOG = LoggerFactory.getLogger(NodeCopier::class.java)
+ }
+}
+
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/NodeFinder.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/NodeFinder.kt
new file mode 100644
index 0000000000..9c2b75d94b
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/NodeFinder.kt
@@ -0,0 +1,32 @@
+package net.corda.bootstrapper.nodes
+
+import com.typesafe.config.ConfigFactory
+import net.corda.bootstrapper.Constants
+import org.slf4j.LoggerFactory
+import java.io.File
+
+class NodeFinder(private val scratchDir: File) {
+
+
+ fun findNodes(): List {
+ return scratchDir.walkBottomUp().filter { it.name == "node.conf" && !it.absolutePath.contains(Constants.BOOTSTRAPPER_DIR_NAME) }.map {
+ try {
+ ConfigFactory.parseFile(it) to it
+ } catch (t: Throwable) {
+ null
+ }
+ }.filterNotNull()
+ .filter { !it.first.hasPath("notary") }
+ .map { (nodeConfig, nodeConfigFile) ->
+ LOG.info("We've found a node with name: ${nodeConfigFile.parentFile.name}")
+ FoundNode(nodeConfigFile, nodeConfigFile.parentFile)
+ }.toList()
+
+ }
+
+ companion object {
+ val LOG = LoggerFactory.getLogger(NodeFinder::class.java)
+ }
+
+}
+
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/NodeInstance.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/NodeInstance.kt
new file mode 100644
index 0000000000..e6d6a440d3
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/NodeInstance.kt
@@ -0,0 +1,31 @@
+package net.corda.bootstrapper.nodes
+
+import net.corda.node.services.config.NodeConfiguration
+import java.io.File
+
+class NodeInstance(configFile: File,
+ baseDirectory: File,
+ copiedNodeConfig: File,
+ copiedNodeDir: File,
+ nodeConfig: NodeConfiguration,
+ localImageId: String,
+ remoteImageName: String,
+ nodeInstanceName: String,
+ actualX500: String,
+ expectedFqName: String,
+ val reachableAddress: String,
+ val portMapping: Map) :
+ NodeInstanceRequest(
+ configFile,
+ baseDirectory,
+ copiedNodeConfig,
+ copiedNodeDir,
+ nodeConfig,
+ localImageId,
+ remoteImageName,
+ nodeInstanceName,
+ actualX500,
+ expectedFqName
+ ) {
+}
+
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/NodeInstanceRequest.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/NodeInstanceRequest.kt
new file mode 100644
index 0000000000..0ecb570352
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/NodeInstanceRequest.kt
@@ -0,0 +1,24 @@
+package net.corda.bootstrapper.nodes
+
+import net.corda.node.services.config.NodeConfiguration
+import java.io.File
+
+open class NodeInstanceRequest(configFile: File, baseDirectory: File,
+ copiedNodeConfig: File, copiedNodeDir: File,
+ nodeConfig: NodeConfiguration, localImageId: String, remoteImageName: String,
+ internal val nodeInstanceName: String,
+ internal val actualX500: String,
+ internal val expectedFqName: String) : PushedNode(
+ configFile, baseDirectory, copiedNodeConfig, copiedNodeDir, nodeConfig, localImageId, remoteImageName
+) {
+
+ override fun toString(): String {
+ return "NodeInstanceRequest(nodeInstanceName='$nodeInstanceName', actualX500='$actualX500', expectedFqName='$expectedFqName') ${super.toString()}"
+ }
+
+ fun toNodeInstance(reachableAddress: String, portMapping: Map): NodeInstance {
+ return NodeInstance(configFile, baseDirectory, copiedNodeConfig, copiedNodeDir, nodeConfig, localImageId, remoteImageName, nodeInstanceName, actualX500, expectedFqName, reachableAddress, portMapping)
+ }
+
+
+}
\ No newline at end of file
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/NodeInstantiator.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/NodeInstantiator.kt
new file mode 100644
index 0000000000..1c7f0ac6f6
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/NodeInstantiator.kt
@@ -0,0 +1,95 @@
+package net.corda.bootstrapper.nodes
+
+import net.corda.bootstrapper.Constants
+import net.corda.bootstrapper.containers.instance.InstanceInfo
+import net.corda.bootstrapper.containers.instance.Instantiator
+import net.corda.bootstrapper.context.Context
+import net.corda.core.identity.CordaX500Name
+import java.util.concurrent.CompletableFuture
+
+class NodeInstantiator(val instantiator: Instantiator,
+ val context: Context) {
+
+
+ fun createInstanceRequests(pushedNode: PushedNode, nodeCount: Map): List {
+
+ val namedMap = nodeCount.map { it.key.name.toLowerCase() to it.value }.toMap()
+
+ return (0 until (namedMap[pushedNode.name.toLowerCase()] ?: 1)).map { i ->
+ createInstanceRequest(pushedNode, i)
+ }
+ }
+
+ private fun createInstanceRequest(node: PushedNode, i: Int): NodeInstanceRequest {
+ val nodeInstanceName = node.name + i
+ val expectedName = instantiator.getExpectedFQDN(nodeInstanceName)
+ return node.toNodeInstanceRequest(nodeInstanceName, buildX500(node.nodeConfig.myLegalName, i), expectedName)
+ }
+
+ fun createInstanceRequest(node: PushedNode): NodeInstanceRequest {
+ return createInstanceRequest(node, 0)
+ }
+
+
+ private fun buildX500(baseX500: CordaX500Name, i: Int): String {
+ if (i == 0) {
+ return baseX500.toString()
+ }
+ return baseX500.copy(commonName = (baseX500.commonName ?: "") + i).toString()
+ }
+
+ fun instantiateNodeInstance(request: Context.PersistableNodeInstance): CompletableFuture {
+ return instantiateNodeInstance(request.remoteImageName, request.rpcPort!!, request.instanceName, request.fqdn, request.instanceX500).thenApplyAsync {
+ InstanceInfo(request.groupName, request.instanceName, request.fqdn, it.first, it.second)
+ }
+ }
+
+ fun instantiateNodeInstance(request: NodeInstanceRequest): CompletableFuture {
+ return instantiateNodeInstance(request.remoteImageName, request.nodeConfig.rpcOptions.address!!.port, request.nodeInstanceName, request.expectedFqName, request.actualX500)
+ .thenApplyAsync { (reachableName, portMapping) ->
+ request.toNodeInstance(reachableName, portMapping)
+ }
+ }
+
+ fun instantiateNotaryInstance(request: NodeInstanceRequest): CompletableFuture {
+ return instantiateNotaryInstance(request.remoteImageName, request.nodeConfig.rpcOptions.address!!.port, request.nodeInstanceName, request.expectedFqName)
+ .thenApplyAsync { (reachableName, portMapping) ->
+ request.toNodeInstance(reachableName, portMapping)
+ }
+ }
+
+ private fun instantiateNotaryInstance(remoteImageName: String,
+ rpcPort: Int,
+ nodeInstanceName: String,
+ expectedFqName: String): CompletableFuture>> {
+ return instantiator.instantiateContainer(
+ remoteImageName,
+ listOf(Constants.NODE_P2P_PORT, Constants.NODE_RPC_PORT, Constants.NODE_SSHD_PORT),
+ nodeInstanceName,
+ mapOf("OUR_NAME" to expectedFqName,
+ "OUR_PORT" to Constants.NODE_P2P_PORT.toString())
+ )
+ }
+
+ private fun instantiateNodeInstance(remoteImageName: String,
+ rpcPort: Int,
+ nodeInstanceName: String,
+ expectedFqName: String,
+ actualX500: String): CompletableFuture>> {
+
+ return instantiator.instantiateContainer(
+ remoteImageName,
+ listOf(Constants.NODE_P2P_PORT, Constants.NODE_RPC_PORT, Constants.NODE_SSHD_PORT),
+ nodeInstanceName,
+ mapOf("OUR_NAME" to expectedFqName,
+ "OUR_PORT" to Constants.NODE_P2P_PORT.toString(),
+ "X500" to actualX500)
+ )
+ }
+
+ fun expectedFqdn(newInstanceName: String): String {
+ return instantiator.getExpectedFQDN(newInstanceName)
+ }
+
+
+}
\ No newline at end of file
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/NodePusher.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/NodePusher.kt
new file mode 100644
index 0000000000..ecb12b363d
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/NodePusher.kt
@@ -0,0 +1,19 @@
+package net.corda.bootstrapper.nodes
+
+import net.corda.bootstrapper.containers.push.ContainerPusher
+import net.corda.bootstrapper.context.Context
+import java.util.concurrent.CompletableFuture
+
+class NodePusher(private val containerPusher: ContainerPusher,
+ private val context: Context) {
+
+
+ fun pushNode(builtNode: BuiltNode): CompletableFuture {
+
+ val localImageId = builtNode.localImageId
+ val nodeImageIdentifier = "node-${builtNode.name}"
+ val nodeImageNameFuture = containerPusher.pushContainerToImageRepository(localImageId,
+ nodeImageIdentifier, context.networkName)
+ return nodeImageNameFuture.thenApplyAsync { imageName -> builtNode.toPushedNode(imageName) }
+ }
+}
\ No newline at end of file
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/PushedNode.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/PushedNode.kt
new file mode 100644
index 0000000000..d75c4390f5
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/PushedNode.kt
@@ -0,0 +1,25 @@
+package net.corda.bootstrapper.nodes
+
+import net.corda.node.services.config.NodeConfiguration
+import java.io.File
+
+open class PushedNode(configFile: File, baseDirectory: File,
+ copiedNodeConfig: File, copiedNodeDir: File,
+ nodeConfig: NodeConfiguration, localImageId: String, val remoteImageName: String) : BuiltNode(
+ configFile,
+ baseDirectory,
+ copiedNodeConfig,
+ copiedNodeDir,
+ nodeConfig,
+ localImageId
+) {
+ fun toNodeInstanceRequest(nodeInstanceName: String, actualX500: String, expectedFqName: String): NodeInstanceRequest {
+ return NodeInstanceRequest(configFile, baseDirectory, copiedNodeConfig, copiedNodeDir, nodeConfig, localImageId, remoteImageName, nodeInstanceName, actualX500, expectedFqName)
+ }
+
+ override fun toString(): String {
+ return "PushedNode(remoteImageName='$remoteImageName') ${super.toString()}"
+ }
+
+
+}
\ No newline at end of file
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/notaries/CopiedNotary.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/notaries/CopiedNotary.kt
new file mode 100644
index 0000000000..f16ef7a70a
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/notaries/CopiedNotary.kt
@@ -0,0 +1,14 @@
+package net.corda.bootstrapper.notaries
+
+import net.corda.bootstrapper.nodes.CopiedNode
+import java.io.File
+
+class CopiedNotary(configFile: File, baseDirectory: File,
+ copiedNodeConfig: File, copiedNodeDir: File, val nodeInfoFile: File) :
+ CopiedNode(configFile, baseDirectory, copiedNodeConfig, copiedNodeDir) {
+}
+
+
+fun CopiedNode.toNotary(nodeInfoFile: File): CopiedNotary {
+ return CopiedNotary(this.configFile, this.baseDirectory, this.copiedNodeConfig, this.copiedNodeDir, nodeInfoFile)
+}
\ No newline at end of file
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/notaries/NotaryCopier.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/notaries/NotaryCopier.kt
new file mode 100644
index 0000000000..e3b51b8e85
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/notaries/NotaryCopier.kt
@@ -0,0 +1,69 @@
+package net.corda.bootstrapper.notaries
+
+import net.corda.bootstrapper.nodes.CopiedNode
+import net.corda.bootstrapper.nodes.FoundNode
+import net.corda.bootstrapper.nodes.NodeCopier
+import org.slf4j.LoggerFactory
+import java.io.File
+
+class NotaryCopier(val cacheDir: File) : NodeCopier(cacheDir) {
+
+ fun copyNotary(foundNotary: FoundNode): CopiedNotary {
+ val nodeCacheDir = File(cacheDir, foundNotary.baseDirectory.name)
+ nodeCacheDir.deleteRecursively()
+ LOG.info("copying: ${foundNotary.baseDirectory} to $nodeCacheDir")
+ foundNotary.baseDirectory.copyRecursively(nodeCacheDir, overwrite = true)
+ copyNotaryBootstrapperFiles(nodeCacheDir)
+ val configInCacheDir = File(nodeCacheDir, "node.conf")
+ LOG.info("Applying precanned config " + configInCacheDir)
+ val rpcSettings = getDefaultRpcSettings()
+ val sshSettings = getDefaultSshSettings();
+ mergeConfigs(configInCacheDir, rpcSettings, sshSettings, Mode.NOTARY)
+ val generatedNodeInfo = generateNodeInfo(nodeCacheDir)
+ return CopiedNode(foundNotary, configInCacheDir, nodeCacheDir).toNotary(generatedNodeInfo)
+ }
+
+ fun generateNodeInfo(dirToGenerateFrom: File): File {
+ val nodeInfoGeneratorProcess = ProcessBuilder()
+ .command(listOf("java", "-jar", "corda.jar", "--just-generate-node-info"))
+ .directory(dirToGenerateFrom)
+ .inheritIO()
+ .start()
+
+ val exitCode = nodeInfoGeneratorProcess.waitFor()
+ if (exitCode != 0) {
+ throw IllegalStateException("Failed to generate nodeInfo for notary: " + dirToGenerateFrom)
+ }
+ val nodeInfoFile = dirToGenerateFrom.listFiles().filter { it.name.startsWith("nodeInfo") }.single()
+ return nodeInfoFile;
+ }
+
+ private fun copyNotaryBootstrapperFiles(nodeCacheDir: File) {
+ this.javaClass.classLoader.getResourceAsStream("notary-Dockerfile").use { nodeDockerFileInStream ->
+ val nodeDockerFile = File(nodeCacheDir, "Dockerfile")
+ nodeDockerFile.outputStream().use { nodeDockerFileOutStream ->
+ nodeDockerFileInStream.copyTo(nodeDockerFileOutStream)
+ }
+ }
+
+ this.javaClass.classLoader.getResourceAsStream("run-corda-notary.sh").use { nodeRunScriptInStream ->
+ val nodeRunScriptFile = File(nodeCacheDir, "run-corda.sh")
+ nodeRunScriptFile.outputStream().use { nodeDockerFileOutStream ->
+ nodeRunScriptInStream.copyTo(nodeDockerFileOutStream)
+ }
+ }
+
+ this.javaClass.classLoader.getResourceAsStream("node_info_watcher.sh").use { nodeRunScriptInStream ->
+ val nodeInfoWatcherFile = File(nodeCacheDir, "node_info_watcher.sh")
+ nodeInfoWatcherFile.outputStream().use { nodeDockerFileOutStream ->
+ nodeRunScriptInStream.copyTo(nodeDockerFileOutStream)
+ }
+ }
+ }
+
+ companion object {
+ val LOG = LoggerFactory.getLogger(NotaryCopier::class.java)
+ }
+
+
+}
\ No newline at end of file
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/notaries/NotaryFinder.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/notaries/NotaryFinder.kt
new file mode 100644
index 0000000000..d4ae23d072
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/notaries/NotaryFinder.kt
@@ -0,0 +1,25 @@
+package net.corda.bootstrapper.notaries
+
+import com.typesafe.config.ConfigFactory
+import net.corda.bootstrapper.Constants
+import net.corda.bootstrapper.nodes.FoundNode
+import java.io.File
+
+class NotaryFinder(private val dirToSearch: File) {
+
+ fun findNotaries(): List {
+ return dirToSearch.walkBottomUp().filter { it.name == "node.conf" && !it.absolutePath.contains(Constants.BOOTSTRAPPER_DIR_NAME) }
+ .map {
+ try {
+ ConfigFactory.parseFile(it) to it
+ } catch (t: Throwable) {
+ null
+ }
+ }.filterNotNull()
+ .filter { it.first.hasPath("notary") }
+ .map { (_, nodeConfigFile) ->
+ FoundNode(nodeConfigFile)
+ }.toList()
+ }
+}
+
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/serialization/SerializationHelper.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/serialization/SerializationHelper.kt
new file mode 100644
index 0000000000..d3d867c461
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/serialization/SerializationHelper.kt
@@ -0,0 +1,30 @@
+package net.corda.bootstrapper.serialization
+
+import net.corda.core.serialization.internal.SerializationEnvironmentImpl
+import net.corda.core.serialization.internal.nodeSerializationEnv
+import net.corda.node.serialization.amqp.AMQPServerSerializationScheme
+import net.corda.serialization.internal.AMQP_P2P_CONTEXT
+import net.corda.serialization.internal.AMQP_STORAGE_CONTEXT
+import net.corda.serialization.internal.SerializationFactoryImpl
+
+
+class SerializationEngine {
+ companion object {
+ fun init() {
+ synchronized(this) {
+ if (nodeSerializationEnv == null) {
+ val classloader = this.javaClass.classLoader
+ nodeSerializationEnv = SerializationEnvironmentImpl(
+ SerializationFactoryImpl().apply {
+ registerScheme(AMQPServerSerializationScheme(emptyList()))
+ },
+ p2pContext = AMQP_P2P_CONTEXT.withClassLoader(classloader),
+ rpcServerContext = AMQP_P2P_CONTEXT.withClassLoader(classloader),
+ storageContext = AMQP_STORAGE_CONTEXT.withClassLoader(classloader),
+ checkpointContext = AMQP_P2P_CONTEXT.withClassLoader(classloader)
+ )
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/volumes/Volume.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/volumes/Volume.kt
new file mode 100644
index 0000000000..fa514dd168
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/volumes/Volume.kt
@@ -0,0 +1,56 @@
+package net.corda.bootstrapper.volumes
+
+import com.microsoft.azure.storage.file.CloudFile
+import com.typesafe.config.ConfigFactory
+import net.corda.bootstrapper.notaries.CopiedNotary
+import net.corda.bootstrapper.serialization.SerializationEngine
+import net.corda.core.node.NetworkParameters
+import net.corda.core.node.NotaryInfo
+import net.corda.core.serialization.deserialize
+import net.corda.nodeapi.internal.DEV_ROOT_CA
+import net.corda.nodeapi.internal.SignedNodeInfo
+import net.corda.nodeapi.internal.createDevNetworkMapCa
+import java.io.File
+import java.security.cert.X509Certificate
+import java.time.Instant
+
+interface Volume {
+ fun notariesForNetworkParams(notaries: List)
+
+ companion object {
+ init {
+ SerializationEngine.init()
+ }
+
+ internal val networkMapCa = createDevNetworkMapCa(DEV_ROOT_CA)
+ internal val networkMapCert: X509Certificate = networkMapCa.certificate
+ internal val keyPair = networkMapCa.keyPair
+
+ }
+
+
+ fun CloudFile.uploadFromByteArray(array: ByteArray) {
+ this.uploadFromByteArray(array, 0, array.size)
+ }
+
+
+ fun convertNodeIntoToNetworkParams(notaryFiles: List>): NetworkParameters {
+ val notaryInfos = notaryFiles.map { (configFile, nodeInfoFile) ->
+ val validating = ConfigFactory.parseFile(configFile).getConfig("notary").getBoolean("validating")
+ nodeInfoFile.readBytes().deserialize().verified().let { NotaryInfo(it.legalIdentities.first(), validating) }
+ }
+
+ return notaryInfos.let {
+ NetworkParameters(
+ minimumPlatformVersion = 1,
+ notaries = it,
+ maxMessageSize = 10485760,
+ maxTransactionSize = Int.MAX_VALUE,
+ modifiedTime = Instant.now(),
+ epoch = 10,
+ whitelistedContractImplementations = emptyMap())
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/volumes/azure/AzureSmbVolume.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/volumes/azure/AzureSmbVolume.kt
new file mode 100644
index 0000000000..185868bbf1
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/volumes/azure/AzureSmbVolume.kt
@@ -0,0 +1,81 @@
+package net.corda.bootstrapper.volumes.azure
+
+import com.microsoft.azure.management.Azure
+import com.microsoft.azure.management.resources.ResourceGroup
+import com.microsoft.azure.management.storage.StorageAccount
+import com.microsoft.azure.storage.CloudStorageAccount
+import net.corda.bootstrapper.Constants.Companion.restFriendlyName
+import net.corda.bootstrapper.notaries.CopiedNotary
+import net.corda.bootstrapper.volumes.Volume
+import net.corda.bootstrapper.volumes.Volume.Companion.keyPair
+import net.corda.bootstrapper.volumes.Volume.Companion.networkMapCert
+import net.corda.core.internal.signWithCert
+import net.corda.core.serialization.serialize
+import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME
+import org.slf4j.LoggerFactory
+
+
+class AzureSmbVolume(private val azure: Azure, private val resourceGroup: ResourceGroup) : Volume {
+
+ private val storageAccount = getStorageAccount()
+
+ private val accKeys = storageAccount.keys[0]
+
+
+ private val cloudFileShare = CloudStorageAccount.parse(
+ "DefaultEndpointsProtocol=https;" +
+ "AccountName=${storageAccount.name()};" +
+ "AccountKey=${accKeys.value()};" +
+ "EndpointSuffix=core.windows.net"
+ )
+ .createCloudFileClient()
+ .getShareReference("nodeinfos")
+
+ val networkParamsFolder = cloudFileShare.rootDirectoryReference.getDirectoryReference("network-params")
+ val shareName: String = cloudFileShare.name
+ val storageAccountName: String
+ get() = resourceGroup.restFriendlyName()
+ val storageAccountKey: String
+ get() = accKeys.value()
+
+
+ init {
+ while (true) {
+ try {
+ cloudFileShare.createIfNotExists()
+ networkParamsFolder.createIfNotExists()
+ break
+ } catch (e: Throwable) {
+ LOG.debug("storage account not ready, waiting")
+ Thread.sleep(5000)
+ }
+ }
+ }
+
+ private fun getStorageAccount(): StorageAccount {
+ return azure.storageAccounts().getByResourceGroup(resourceGroup.name(), resourceGroup.restFriendlyName())
+ ?: azure.storageAccounts().define(resourceGroup.restFriendlyName())
+ .withRegion(resourceGroup.region())
+ .withExistingResourceGroup(resourceGroup)
+ .withAccessFromAllNetworks()
+ .create()
+ }
+
+ override fun notariesForNetworkParams(notaries: List) {
+ val networkParamsFile = networkParamsFolder.getFileReference(NETWORK_PARAMS_FILE_NAME)
+ networkParamsFile.deleteIfExists()
+ LOG.info("Storing network-params in AzureFile location: " + networkParamsFile.uri)
+ val networkParameters = convertNodeIntoToNetworkParams(notaries.map { it.configFile to it.nodeInfoFile })
+ networkParamsFile.uploadFromByteArray(networkParameters.signWithCert(keyPair.private, networkMapCert).serialize().bytes)
+ }
+
+
+ companion object {
+ val LOG = LoggerFactory.getLogger(AzureSmbVolume::class.java)
+ }
+
+}
+
+
+
+
diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/volumes/docker/LocalVolume.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/volumes/docker/LocalVolume.kt
new file mode 100644
index 0000000000..e5ca96c9a0
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/volumes/docker/LocalVolume.kt
@@ -0,0 +1,35 @@
+package net.corda.bootstrapper.volumes.docker
+
+import net.corda.bootstrapper.context.Context
+import net.corda.bootstrapper.notaries.CopiedNotary
+import net.corda.bootstrapper.volumes.Volume
+import net.corda.core.internal.signWithCert
+import net.corda.core.serialization.serialize
+import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME
+import org.slf4j.LoggerFactory
+import java.io.File
+
+class LocalVolume(scratchDir: File, context: Context) : Volume {
+
+ private val networkDir = File(scratchDir, context.safeNetworkName)
+ private val volumeDir = File(networkDir, "nodeinfos")
+ private val networkParamsDir = File(volumeDir, "network-params")
+
+ override fun notariesForNetworkParams(notaries: List) {
+ volumeDir.deleteRecursively()
+ networkParamsDir.mkdirs()
+ val networkParameters = convertNodeIntoToNetworkParams(notaries.map { it.configFile to it.nodeInfoFile })
+ val networkParamsFile = File(networkParamsDir, NETWORK_PARAMS_FILE_NAME)
+ networkParamsFile.outputStream().use { networkParameters.signWithCert(Volume.keyPair.private, Volume.networkMapCert).serialize().writeTo(it) }
+ LOG.info("wrote network params to local file: ${networkParamsFile.absolutePath}")
+ }
+
+
+ fun getPath(): String {
+ return volumeDir.absolutePath
+ }
+
+ companion object {
+ val LOG = LoggerFactory.getLogger(LocalVolume::class.java)
+ }
+}
\ No newline at end of file
diff --git a/tools/network-bootstrapper/src/main/resources/node-Dockerfile b/tools/network-bootstrapper/src/main/resources/node-Dockerfile
new file mode 100644
index 0000000000..3ef62097b2
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/resources/node-Dockerfile
@@ -0,0 +1,37 @@
+# Base image from (http://phusion.github.io/baseimage-docker)
+FROM openjdk:8u151-jre-alpine
+
+RUN apk upgrade --update && \
+ apk add --update --no-cache bash iputils && \
+ rm -rf /var/cache/apk/* && \
+ # Add user to run the app && \
+ addgroup corda && \
+ adduser -G corda -D -s /bin/bash corda && \
+ # Create /opt/corda directory && \
+ mkdir -p /opt/corda/plugins && \
+ mkdir -p /opt/corda/logs && \
+ mkdir -p /opt/corda/additional-node-infos && \
+ mkdir -p /opt/node-setup
+
+# Copy corda files
+ADD --chown=corda:corda corda.jar /opt/corda/corda.jar
+ADD --chown=corda:corda node.conf /opt/corda/node.conf
+ADD --chown=corda:corda cordapps/ /opt/corda/cordapps
+
+# Copy node info watcher script
+ADD --chown=corda:corda node_info_watcher.sh /opt/corda/
+
+COPY run-corda.sh /run-corda.sh
+
+RUN chmod +x /run-corda.sh && \
+ chmod +x /opt/corda/node_info_watcher.sh && \
+ sync && \
+ chown -R corda:corda /opt/corda
+
+# Working directory for Corda
+WORKDIR /opt/corda
+ENV HOME=/opt/corda
+USER corda
+
+# Start it
+CMD ["/run-corda.sh"]
\ No newline at end of file
diff --git a/tools/network-bootstrapper/src/main/resources/node_info_watcher.sh b/tools/network-bootstrapper/src/main/resources/node_info_watcher.sh
new file mode 100755
index 0000000000..a3b6e19387
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/resources/node_info_watcher.sh
@@ -0,0 +1,12 @@
+#!/bin/bash
+while [ 1 -lt 2 ]; do
+ NODE_INFO=$(ls | grep nodeInfo)
+ if [ ${#NODE_INFO} -ge 5 ]; then
+ echo "found nodeInfo copying to additional node node info folder"
+ cp ${NODE_INFO} additional-node-infos/
+ exit 0
+ else
+ echo "no node info found waiting"
+ fi
+ sleep 5
+done
\ No newline at end of file
diff --git a/tools/network-bootstrapper/src/main/resources/node_killer.sh b/tools/network-bootstrapper/src/main/resources/node_killer.sh
new file mode 100755
index 0000000000..e553d287c5
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/resources/node_killer.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+sleep $1
\ No newline at end of file
diff --git a/tools/network-bootstrapper/src/main/resources/notary-Dockerfile b/tools/network-bootstrapper/src/main/resources/notary-Dockerfile
new file mode 100644
index 0000000000..d8a7b8a0a7
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/resources/notary-Dockerfile
@@ -0,0 +1,39 @@
+# Base image from (http://phusion.github.io/baseimage-docker)
+FROM openjdk:8u151-jre-alpine
+
+RUN apk upgrade --update && \
+ apk add --update --no-cache bash iputils && \
+ rm -rf /var/cache/apk/* && \
+ # Add user to run the app && \
+ addgroup corda && \
+ adduser -G corda -D -s /bin/bash corda && \
+ # Create /opt/corda directory && \
+ mkdir -p /opt/corda/plugins && \
+ mkdir -p /opt/corda/logs && \
+ mkdir -p /opt/corda/additional-node-infos && \
+ mkdir -p /opt/node-setup
+
+# Copy corda files
+ADD --chown=corda:corda corda.jar /opt/corda/corda.jar
+ADD --chown=corda:corda node.conf /opt/corda/node.conf
+ADD --chown=corda:corda cordapps/ /opt/corda/cordapps
+ADD --chown=corda:corda certificates/ /opt/corda/certificates
+#ADD --chown=corda:corda nodeInfo-* /opt/corda/
+
+# Copy node info watcher script
+ADD --chown=corda:corda node_info_watcher.sh /opt/corda/
+
+COPY run-corda.sh /run-corda.sh
+
+RUN chmod +x /run-corda.sh && \
+ chmod +x /opt/corda/node_info_watcher.sh && \
+ sync && \
+ chown -R corda:corda /opt/corda
+
+# Working directory for Corda
+WORKDIR /opt/corda
+ENV HOME=/opt/corda
+USER corda
+
+# Start it
+CMD ["/run-corda.sh"]
\ No newline at end of file
diff --git a/tools/network-bootstrapper/src/main/resources/rpc-settings.conf b/tools/network-bootstrapper/src/main/resources/rpc-settings.conf
new file mode 100644
index 0000000000..95c820218f
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/resources/rpc-settings.conf
@@ -0,0 +1,4 @@
+rpcSettings {
+ address="0.0.0.0:10003"
+ adminAddress="127.0.0.1:10005"
+}
\ No newline at end of file
diff --git a/tools/network-bootstrapper/src/main/resources/run-corda-node.sh b/tools/network-bootstrapper/src/main/resources/run-corda-node.sh
new file mode 100755
index 0000000000..c5d379d260
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/resources/run-corda-node.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+: ${CORDA_HOME:=/opt/corda}
+: ${JAVA_OPTIONS:=-Xmx512m}
+: ${X500? Need a value for the x500 name of this node}
+: ${OUR_NAME? Need a value for the expected FQDN of this node}
+: ${OUR_PORT? Need a value for the port to bind to}
+
+export CORDA_HOME JAVA_OPTIONS
+
+sed -i "/myLegalName/d" node.conf
+echo "myLegalName=\"${X500}\"" >> node.conf
+echo "p2pAddress=\"${OUR_NAME}:${OUR_PORT}\"" >> node.conf
+
+cp additional-node-infos/network-params/network-parameters .
+
+bash node_info_watcher.sh &
+
+cd ${CORDA_HOME}
+
+if java ${JAVA_OPTIONS} -jar ${CORDA_HOME}/corda.jar 2>&1 ; then
+ echo "Corda exited with zero exit code"
+else
+ echo "Corda exited with nonzero exit code, sleeping to allow log collection"
+ sleep 10000
+fi
\ No newline at end of file
diff --git a/tools/network-bootstrapper/src/main/resources/run-corda-notary.sh b/tools/network-bootstrapper/src/main/resources/run-corda-notary.sh
new file mode 100755
index 0000000000..ec8e21d4e7
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/resources/run-corda-notary.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+
+: ${CORDA_HOME:=/opt/corda}
+: ${JAVA_OPTIONS:=-Xmx512m}
+: ${OUR_NAME? Need a value for the expected FQDN of this node}
+: ${OUR_PORT? Need a value for the port to bind to}
+
+export CORDA_HOME JAVA_OPTIONS
+echo "p2pAddress=\"${OUR_NAME}:${OUR_PORT}\"" >> node.conf
+cp additional-node-infos/network-params/network-parameters .
+
+bash node_info_watcher.sh &
+
+cd ${CORDA_HOME}
+
+
+if java ${JAVA_OPTIONS} -jar ${CORDA_HOME}/corda.jar 2>&1 ; then
+ echo "Corda exited with zero exit code"
+else
+ echo "Corda exited with nonzero exit code, sleeping to allow log collection"
+ sleep 10000
+fi
+
+
+
diff --git a/tools/network-bootstrapper/src/main/resources/ssh.conf b/tools/network-bootstrapper/src/main/resources/ssh.conf
new file mode 100644
index 0000000000..32f59a0321
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/resources/ssh.conf
@@ -0,0 +1,3 @@
+sshd {
+ port = 12222
+}
\ No newline at end of file