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, String> { + var networkName1 = RandomStringUtils.randomAlphabetic(4) + "-network" + val textInputDialog = TextInputDialog(networkName1) + textInputDialog.title = "Azure Resource Group" + networkName1 = textInputDialog.showAndWait().orElseGet { networkName1 } + return Pair(mapOf(Constants.REGION_ARG_NAME to ChoiceDialog(Region.EUROPE_WEST, Region.values().toList().sortedBy { it.name() }).showAndWait().get().name()), networkName1) + } + + private fun translateForPrinting(selectedItem: NodeTemplateInfo): Any { + return object { + val templateId = selectedItem.templateId.get() + val nodeType = selectedItem.nodeType.get() + val localDockerImageId = selectedItem.localDockerImageId.get() + val repositoryImageId = selectedItem.repositoryImageId.get() + val status = selectedItem.status.get() + val instances = selectedItem.instances.map { it } + } + } + + @FXML + fun onOpenClicked() { + val chooser = DirectoryChooser() + chooser.initialDirectory = File(System.getProperty("user.home")) + val file: File = chooser.showDialog(null) ?: return // Null means user cancelled. + processSelectedDirectory(file) + } + + private fun processSelectedDirectory(dir: File) { + controller.clearAll() + controller.baseDir.set(dir) + val foundNodes = CompletableFuture.supplyAsync { + val nodeFinder = NodeFinder(dir) + nodeFinder.findNodes() + } + val foundNotaries = CompletableFuture.supplyAsync { + val notaryFinder = NotaryFinder(dir) + notaryFinder.findNotaries() + } + foundNodes.thenCombine(foundNotaries) { nodes, notaries -> + notaries to nodes + }.thenAcceptAsync({ (notaries: List, nodes: List) -> + runLater { + controller.foundNodes(nodes) + controller.notaries(notaries) + } + }) + } + + class NodeTemplateInfo(templateId: String, type: NodeType) { + val templateId: SimpleStringProperty = object : SimpleStringProperty(templateId) { + override fun toString(): String { + return this.get()?.toString() ?: "null" + } + } + val nodeType: SimpleObjectProperty = SimpleObjectProperty(type) + val localDockerImageId: SimpleStringProperty = SimpleStringProperty() + val repositoryImageId: SimpleStringProperty = SimpleStringProperty() + val status: SimpleObjectProperty = SimpleObjectProperty(NodeBuildStatus.DISCOVERED) + val instances: MutableList = ArrayList() + val numberOfInstancesWaiting: AtomicInteger = AtomicInteger(-1) + } + + enum class NodeBuildStatus { + DISCOVERED, LOCALLY_BUILDING, LOCALLY_BUILT, REMOTE_PUSHING, REMOTE_PUSHED, INSTANTIATING, INSTANTIATED, + } + + enum class NodeType { + NODE, NOTARY + } + + class State : Controller() { + val foundNodes = Collections.synchronizedList(ArrayList()).observable() + val foundNotaries = Collections.synchronizedList(ArrayList()).observable() + val networkContext = SimpleObjectProperty(null) + + val unsortedNodes = Collections.synchronizedList(ArrayList()).observable() + val sortedNodes = SortedList(unsortedNodes, Comparator { o1, o2 -> + compareValues(o1.nodeType.toString() + o1.templateId, o2.nodeType.toString() + o2.templateId) * -1 + }) + + fun clear() { + networkContext.set(null) + } + + fun clearAll() { + networkContext.set(null) + foundNodes.clear() + foundNotaries.clear() + unsortedNodes.clear() + } + + fun foundNodes(nodesToAdd: List) { + foundNodes.clear() + nodesToAdd.forEach { + runLater { + foundNodes.add(FoundNodeTableEntry(it.name)) + unsortedNodes.add(NodeTemplateInfo(it.name, NodeType.NODE)) + } + } + } + + fun notaries(notaries: List) { + foundNotaries.clear() + notaries.forEach { + runLater { + foundNotaries.add(it) + unsortedNodes.add(NodeTemplateInfo(it.name, NodeType.NOTARY)) + } + } + + } + + var baseDir = SimpleObjectProperty(null) + + fun addBuiltNode(builtNode: BuiltNode) { + runLater { + val foundNode = unsortedNodes.find { it.templateId.get() == builtNode.name } + foundNode?.status?.set(NodeBuildStatus.LOCALLY_BUILT) + foundNode?.localDockerImageId?.set(builtNode.localImageId) + } + } + + fun addPushedNode(pushedNode: PushedNode) { + runLater { + val foundNode = unsortedNodes.find { it.templateId.get() == pushedNode.name } + foundNode?.status?.set(NodeBuildStatus.REMOTE_PUSHED) + foundNode?.repositoryImageId?.set(pushedNode.remoteImageName) + + } + } + + fun onBuild(nodeBuilding: FoundNode) { + val foundNode = unsortedNodes.find { it.templateId.get() == nodeBuilding.name } + foundNode?.status?.set(NodeBuildStatus.LOCALLY_BUILDING) + } + + fun addInstance(nodeInstance: NodeInstance) { + addInstance(NodeInstanceEntry( + 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 addInstanceRequests(requests: List) { + requests.firstOrNull()?.let { request -> + unsortedNodes.find { it.templateId.get() == request.name }?.let { + it.numberOfInstancesWaiting.set(requests.size) + it.status.set(NodeBuildStatus.INSTANTIATING) + } + } + } + + fun addInstance(nodeInstance: NodeInstanceEntry) { + runLater { + val foundNode = unsortedNodes.find { it.templateId.get() == nodeInstance.id } + foundNode?.instances?.add(nodeInstance) + if (foundNode != null && foundNode.instances.size == foundNode.numberOfInstancesWaiting.get()) { + foundNode.status.set(NodeBuildStatus.INSTANTIATED) + } + } + } + + fun addInstanceRequest(nodeToAdd: String) { + val foundNode = unsortedNodes.find { it.templateId.get() == nodeToAdd } + foundNode?.numberOfInstancesWaiting?.incrementAndGet() + foundNode?.status?.set(NodeBuildStatus.INSTANTIATING) + } + } + + data class NodeInstanceEntry(val id: String, + val nodeInstanceName: String, + val address: String, + val locallyReachableAddress: String, + val rpcPort: Int, + val sshPort: Int) + +} + +data class FoundNodeTableEntry(val id: String, @Volatile var count: Int = 1) \ 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..841022f502 --- /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.stage.Stage +import tornadofx.* + +class Gui : App(BootstrapperView::class) { + override fun start(stage: Stage) { + super.start(stage) + stage.scene.stylesheets.add("/views/bootstrapper.css") + } +} 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..9d9bca13b3 --- /dev/null +++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/NodeCopier.kt @@ -0,0 +1,102 @@ +package net.corda.bootstrapper.nodes + +import com.typesafe.config.ConfigFactory +import com.typesafe.config.ConfigRenderOptions +import com.typesafe.config.ConfigValue +import net.corda.bootstrapper.useAndClose +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").useAndClose { nodeDockerFileInStream -> + val nodeDockerFile = File(nodeCacheDir, "Dockerfile") + nodeDockerFile.outputStream().useAndClose { nodeDockerFileOutStream -> + nodeDockerFileInStream.copyTo(nodeDockerFileOutStream) + } + } + + this.javaClass.classLoader.getResourceAsStream("run-corda-node.sh").useAndClose { nodeRunScriptInStream -> + val nodeRunScriptFile = File(nodeCacheDir, "run-corda.sh") + nodeRunScriptFile.outputStream().useAndClose { nodeDockerFileOutStream -> + nodeRunScriptInStream.copyTo(nodeDockerFileOutStream) + } + } + + this.javaClass.classLoader.getResourceAsStream("node_info_watcher.sh").useAndClose { nodeRunScriptInStream -> + val nodeInfoWatcherFile = File(nodeCacheDir, "node_info_watcher.sh") + nodeInfoWatcherFile.outputStream().useAndClose { nodeDockerFileOutStream -> + nodeRunScriptInStream.copyTo(nodeDockerFileOutStream) + } + } + } + + internal fun getDefaultRpcSettings(): ConfigValue { + return javaClass + .classLoader + .getResourceAsStream("rpc-settings.conf") + .reader().useAndClose { + ConfigFactory.parseReader(it) + }.getValue("rpcSettings") + } + + internal fun getDefaultSshSettings(): ConfigValue { + return javaClass + .classLoader + .getResourceAsStream("ssh.conf") + .reader().useAndClose { + 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().useAndClose { + 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..ffc949b584 --- /dev/null +++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/notaries/NotaryCopier.kt @@ -0,0 +1,70 @@ +package net.corda.bootstrapper.notaries + +import net.corda.bootstrapper.nodes.CopiedNode +import net.corda.bootstrapper.nodes.FoundNode +import net.corda.bootstrapper.nodes.NodeCopier +import net.corda.bootstrapper.useAndClose +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").useAndClose { nodeDockerFileInStream -> + val nodeDockerFile = File(nodeCacheDir, "Dockerfile") + nodeDockerFile.outputStream().useAndClose { nodeDockerFileOutStream -> + nodeDockerFileInStream.copyTo(nodeDockerFileOutStream) + } + } + + this.javaClass.classLoader.getResourceAsStream("run-corda-notary.sh").useAndClose { nodeRunScriptInStream -> + val nodeRunScriptFile = File(nodeCacheDir, "run-corda.sh") + nodeRunScriptFile.outputStream().useAndClose { nodeDockerFileOutStream -> + nodeRunScriptInStream.copyTo(nodeDockerFileOutStream) + } + } + + this.javaClass.classLoader.getResourceAsStream("node_info_watcher.sh").useAndClose { nodeRunScriptInStream -> + val nodeInfoWatcherFile = File(nodeCacheDir, "node_info_watcher.sh") + nodeInfoWatcherFile.outputStream().useAndClose { 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..f004ee9e24 --- /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.nodeapi.internal.serialization.AMQP_P2P_CONTEXT +import net.corda.nodeapi.internal.serialization.AMQP_STORAGE_CONTEXT +import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl +import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme + + +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..130835b179 --- /dev/null +++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/volumes/docker/LocalVolume.kt @@ -0,0 +1,39 @@ +package net.corda.bootstrapper.volumes.docker + +import net.corda.bootstrapper.context.Context +import net.corda.bootstrapper.notaries.CopiedNotary +import net.corda.bootstrapper.useAndClose +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().useAndClose { fileOutputStream -> + val serializedNetworkParams = networkParameters.signWithCert(Volume.keyPair.private, Volume.networkMapCert).serialize() + fileOutputStream.write(serializedNetworkParams.bytes) + } + 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 diff --git a/tools/network-bootstrapper/src/main/resources/views/bootstrapper.css b/tools/network-bootstrapper/src/main/resources/views/bootstrapper.css new file mode 100644 index 0000000000..b02c3130b5 --- /dev/null +++ b/tools/network-bootstrapper/src/main/resources/views/bootstrapper.css @@ -0,0 +1,13 @@ +.top-pane { + -fx-background-color: white; +} + +.top-pane > .button, .top-pane .toggle-button { + -fx-padding: 15px; + -fx-base: white; +} + +.top-pane > .choice-box { + -fx-padding: 10px; + -fx-base: white; +} \ No newline at end of file diff --git a/tools/network-bootstrapper/src/main/resources/views/cordalogo.png b/tools/network-bootstrapper/src/main/resources/views/cordalogo.png new file mode 100644 index 0000000000..5c97ca01e9 Binary files /dev/null and b/tools/network-bootstrapper/src/main/resources/views/cordalogo.png differ diff --git a/tools/network-bootstrapper/src/main/resources/views/mainPane.fxml b/tools/network-bootstrapper/src/main/resources/views/mainPane.fxml new file mode 100644 index 0000000000..c62993fd25 --- /dev/null +++ b/tools/network-bootstrapper/src/main/resources/views/mainPane.fxml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +