diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index a00ce9d70a..93a42e1184 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -101,6 +101,8 @@
+
+
diff --git a/settings.gradle b/settings.gradle
index a3887c5abd..e1be37ecbc 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -35,6 +35,7 @@ include 'tools:loadtest'
include 'tools:graphs'
include 'tools:bootstrapper'
include 'tools:blobinspector'
+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/network-bootstrapper/build.gradle b/tools/network-bootstrapper/build.gradle
new file mode 100644
index 0000000000..2b0089499b
--- /dev/null
+++ b/tools/network-bootstrapper/build.gradle
@@ -0,0 +1,69 @@
+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"
+
+ // ControlsFX: Extra controls for JavaFX.
+ 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..129d56f25b
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/Constants.kt
@@ -0,0 +1,57 @@
+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
+import java.io.Closeable
+
+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()
+ }
+ }
+}
+
+fun T.useAndClose(block: (T) -> R): R {
+ return try {
+ block(this)
+ } finally {
+ this.close()
+ }
+}
\ 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..eb38bf11cb
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt
@@ -0,0 +1,34 @@
+@file:JvmName("Main")
+package net.corda.bootstrapper
+
+import javafx.application.Application
+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.gui.Gui
+import net.corda.bootstrapper.serialization.SerializationEngine
+import picocli.CommandLine
+
+val baseArgs = CliParser()
+
+fun main(args: Array) {
+ SerializationEngine.init()
+ CommandLine(baseArgs).parse(*args)
+
+ if (baseArgs.gui) {
+ Application.launch(Gui::class.java)
+ return
+ }
+
+ 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..6b4ee0220f
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/NetworkBuilder.kt
@@ -0,0 +1,234 @@
+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>>
+ fun onNodeStartBuild(callback: (FoundNode) -> Unit): NetworkBuilder
+ fun onNodePushStart(callback: (BuiltNode) -> Unit): NetworkBuilder
+ fun onNodeInstancesRequested(callback: (List) -> Unit): NetworkBuilder
+
+}
+
+private class NetworkBuilderImpl : NetworkBuilder {
+
+
+ @Volatile
+ private var onNodeLocatedCallback: ((FoundNode) -> Unit) = {}
+ @Volatile
+ private var onNodeCopiedCallback: ((CopiedNode) -> Unit) = {}
+ @Volatile
+ private var onNodeBuildStartCallback: (FoundNode) -> Unit = {}
+ @Volatile
+ private var onNodeBuiltCallback: ((BuiltNode) -> Unit) = {}
+ @Volatile
+ private var onNodePushStartCallback: ((BuiltNode) -> Unit) = {}
+ @Volatile
+ private var onNodePushedCallback: ((PushedNode) -> Unit) = {}
+ @Volatile
+ private var onNodeInstanceRequestedCallback: (List) -> 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 onNodeStartBuild(callback: (FoundNode) -> Unit): NetworkBuilder {
+ this.onNodeBuildStartCallback = 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 onNodeInstancesRequested(callback: (List) -> Unit): NetworkBuilder {
+ this.onNodeInstanceRequestedCallback = 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 onNodePushStart(callback: (BuiltNode) -> Unit): NetworkBuilder {
+ this.onNodePushStartCallback = callback;
+ 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 ->
+ onNodeBuildStartCallback.invoke(foundNode)
+ notaryCopier.copyNotary(foundNode)
+ }
+ volume.notariesForNetworkParams(copiedNotaries)
+ copiedNotaries
+ }
+
+ val notariesFuture = notaryDiscoveryFuture.thenCompose { copiedNotaries ->
+ copiedNotaries
+ .map { copiedNotary ->
+ nodeBuilder.buildNode(copiedNotary).also(onNodeBuiltCallback)
+ }.map { builtNotary ->
+ onNodePushStartCallback(builtNotary)
+ nodePusher.pushNode(builtNotary).thenApply { it.also(onNodePushedCallback) }
+ }.map { pushedNotary ->
+ pushedNotary.thenApplyAsync { nodeInstantiator.createInstanceRequest(it).also { onNodeInstanceRequestedCallback.invoke(listOf(it)) } }
+ }.map { instanceRequest ->
+ instanceRequest.thenComposeAsync { request ->
+ nodeInstantiator.instantiateNotaryInstance(request).thenApply { it.also(onNodeInstanceCallback) }
+ }
+ }.toSingleFuture()
+ }
+
+ val nodesFuture = notaryDiscoveryFuture.thenCombineAsync(nodeDiscoveryFuture) { _, nodeCount ->
+ nodeCount.keys
+ .map { foundNode ->
+ nodeCopier.copyNode(foundNode).let {
+ onNodeCopiedCallback.invoke(it)
+ it
+ }
+ }.map { copiedNode: CopiedNode ->
+ onNodeBuildStartCallback.invoke(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).also(onNodeInstanceRequestedCallback)
+
+ }
+ }.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..2737411593
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/backends/Backend.kt
@@ -0,0 +1,48 @@
+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(val displayName: String) {
+
+ AZURE("Azure Containers"), LOCAL_DOCKER("Local Docker");
+
+
+ override fun toString(): String {
+ return this.displayName
+ }
+
+
+ }
+
+ 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..2f0f6e2080
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/cli/CommandLineInterface.kt
@@ -0,0 +1,70 @@
+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.bootstrapper.useAndClose
+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 ?: "corda-network"
+ 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().useAndClose {
+ 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().useAndClose {
+ 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..982e8146ae
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/cli/CommandParsers.kt
@@ -0,0 +1,55 @@
+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 CliParser {
+ @Option(names = arrayOf("-n", "--network-name"), description = arrayOf("The resource grouping to use"))
+ var name: String? = null
+
+ @Option(names = arrayOf("-g", "--gui"), description = arrayOf("Run the graphical user interface"))
+ var gui = false
+
+ @Option(names = arrayOf("-d", "--nodes-directory"), description = arrayOf("The directory to search for nodes in"))
+ var baseDirectory = File(System.getProperty("user.dir"))
+
+ @Option(names = arrayOf("-b", "--backend"), description = arrayOf("The backend to use when instantiating nodes"))
+ var backendType: Backend.BackendType = Backend.BackendType.LOCAL_DOCKER
+
+ @Option(names = arrayOf("--nodes"), split = ":", description = arrayOf("The number of each node to create. NodeX:2 will create two instances of NodeX"))
+ var nodes: MutableMap = hashMapOf()
+
+ @Option(names = arrayOf("--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 = arrayOf("-r", "--region"), description = arrayOf("The azure region to use"), converter = arrayOf(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..c353047c66
--- /dev/null
+++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/gui/BootstrapperView.kt
@@ -0,0 +1,361 @@
+package net.corda.bootstrapper.gui
+
+import com.microsoft.azure.management.resources.fluentcore.arm.Region
+import javafx.beans.property.SimpleObjectProperty
+import javafx.beans.property.SimpleStringProperty
+import javafx.collections.ObservableListBase
+import javafx.collections.transformation.SortedList
+import javafx.event.EventHandler
+import javafx.fxml.FXML
+import javafx.scene.control.*
+import javafx.scene.input.MouseEvent
+import javafx.scene.layout.HBox
+import javafx.scene.layout.Priority
+import javafx.scene.layout.VBox
+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 org.controlsfx.control.SegmentedButton
+import tornadofx.*
+import java.io.File
+import java.util.*
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.atomic.AtomicInteger
+import kotlin.Comparator
+import kotlin.collections.ArrayList
+
+class BootstrapperView : View("Corda Network Builder") {
+ val YAML_MAPPER = Constants.getContextMapper()
+ override val root: VBox by fxml("/views/mainPane.fxml")
+
+ val controller: State by inject()
+
+ val localDockerBtn: ToggleButton by fxid()
+ val azureBtn: ToggleButton by fxid()
+ val nodeTableView: TableView by fxid()
+ val templateChoiceBox: ChoiceBox by fxid()
+ val buildButton: Button by fxid()
+ val addInstanceButton: Button by fxid()
+ val infoTextArea: TextArea by fxid()
+
+ init {
+ visuallyTweakBackendSelector()
+
+ buildButton.run {
+ enableWhen { controller.baseDir.isNotNull }
+ action {
+ var networkName = "corda-network"
+
+ val selectedBackEnd = when {
+ azureBtn.isSelected -> Backend.BackendType.AZURE
+ localDockerBtn.isSelected -> Backend.BackendType.LOCAL_DOCKER
+ else -> kotlin.error("Unknown backend selected")
+ }
+
+ val backendParams = when (selectedBackEnd) {
+ Backend.BackendType.LOCAL_DOCKER -> {
+ emptyMap()
+ }
+ Backend.BackendType.AZURE -> {
+ val pair = setupAzureRegionOptions()
+ networkName = pair.second
+ pair.first
+ }
+ }
+
+ val nodeCount = controller.foundNodes.map { it.id to it.count }.toMap()
+ val result = NetworkBuilder.instance()
+ .withBasedir(controller.baseDir.get())
+ .withNetworkName(networkName)
+ .onNodeStartBuild(controller::onBuild)
+ .onNodeBuild(controller::addBuiltNode)
+ .onNodePushStart(controller::addBuiltNode)
+ .onNodePushed(controller::addPushedNode)
+ .onNodeInstancesRequested(controller::addInstanceRequests)
+ .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)
+ }
+ }
+ }
+ }
+ }
+
+ templateChoiceBox.run {
+ enableWhen { controller.networkContext.isNotNull }
+ controller.networkContext.addListener { _, _, newValue ->
+ if (newValue != null) {
+ items = object : ObservableListBase() {
+ override fun get(index: Int): String {
+ return controller.foundNodes[index].id
+ }
+
+ override val size: Int
+ get() = controller.foundNodes.size
+ }
+ selectionModel.select(controller.foundNodes[0].id)
+ }
+ }
+ }
+
+ addInstanceButton.run {
+ enableWhen { controller.networkContext.isNotNull }
+ action {
+ templateChoiceBox.selectionModel.selectedItem?.let { nodeToAdd ->
+ val context = controller.networkContext.value
+ runLater {
+ val (_, instantiator, _) = Backend.fromContext(
+ context,
+ File(controller.baseDir.get(), Constants.BOOTSTRAPPER_DIR_NAME))
+ val nodeAdder = NodeAdder(context, NodeInstantiator(instantiator, context))
+ controller.addInstanceRequest(nodeToAdd)
+ nodeAdder.addNode(context, nodeToAdd).handleAsync { instanceInfo, t ->
+ t?.let {
+ GuiUtils.showException("Failed", "Failed to add node", it)
+ }
+ instanceInfo?.let {
+ runLater {
+ controller.addInstance(NodeInstanceEntry(
+ 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))
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ nodeTableView.run {
+ items = controller.sortedNodes
+ column("ID", NodeTemplateInfo::templateId)
+ column("Type", NodeTemplateInfo::nodeType)
+ column("Local Docker Image", NodeTemplateInfo::localDockerImageId)
+ column("Repository Image", NodeTemplateInfo::repositoryImageId)
+ column("Status", NodeTemplateInfo::status)
+ columnResizePolicy = TableView.CONSTRAINED_RESIZE_POLICY
+ hgrow = Priority.ALWAYS
+
+ onMouseClicked = EventHandler { _ ->
+ val selectedItem: NodeTemplateInfo = selectionModel.selectedItem ?: return@EventHandler
+ infoTextArea.text = YAML_MAPPER.writeValueAsString(translateForPrinting(selectedItem))
+ }
+ }
+ }
+
+ private fun visuallyTweakBackendSelector() {
+ // The SegmentedButton will jam together the two toggle buttons in a way
+ // that looks more modern.
+ val hBox = localDockerBtn.parent as HBox
+ val idx = hBox.children.indexOf(localDockerBtn)
+ // Adding this to the hbox will re-parent the two toggle buttons into the
+ // SegmentedButton control, so we have to put it in the same position as
+ // the original buttons. Unfortunately it's not so Scene Builder friendly.
+ hBox.children.add(idx, SegmentedButton(localDockerBtn, azureBtn).apply {
+ styleClass.add(SegmentedButton.STYLE_CLASS_DARK)
+ })
+ }
+
+ private fun setupAzureRegionOptions(): Pair