CORDA-1717 - backport bootstrapper to 3.2 (#3496) (#3602)

This commit is contained in:
Katelyn Baker 2018-07-13 18:13:24 +01:00 committed by GitHub
parent 9259c9ec45
commit 5772208c7c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 2579 additions and 0 deletions

2
.idea/compiler.xml generated
View File

@ -101,6 +101,8 @@
<module name="loadtest_test" target="1.8" />
<module name="mock_main" target="1.8" />
<module name="mock_test" target="1.8" />
<module name="network-bootstrapper_main" target="1.8" />
<module name="network-bootstrapper_test" target="1.8" />
<module name="network-visualiser_main" target="1.8" />
<module name="network-visualiser_test" target="1.8" />
<module name="node-api_main" target="1.8" />

View File

@ -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'

View File

@ -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"
}

View File

@ -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();
}
}

View File

@ -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<Region>() {
override fun serialize(value: Region, gen: JsonGenerator, serializers: SerializerProvider?) {
gen.writeString(value.name())
}
})
it.addDeserializer(Region::class.java, object : JsonDeserializer<Region>() {
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 : Closeable, R> T.useAndClose(block: (T) -> R): R {
return try {
block(this)
} finally {
this.close()
}
}

View File

@ -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<String>) {
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)
}

View File

@ -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<String, Int>): NetworkBuilder
fun withNetworkName(networtName: String): NetworkBuilder
fun withBasedir(baseDir: File): NetworkBuilder
fun withBackend(backendType: Backend.BackendType): NetworkBuilder
fun withBackendOptions(options: Map<String, String>): NetworkBuilder
fun build(): CompletableFuture<Pair<List<NodeInstance>, Context>>
fun onNodeStartBuild(callback: (FoundNode) -> Unit): NetworkBuilder
fun onNodePushStart(callback: (BuiltNode) -> Unit): NetworkBuilder
fun onNodeInstancesRequested(callback: (List<NodeInstanceRequest>) -> 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<NodeInstanceRequest>) -> Unit = {}
@Volatile
private var onNodeInstanceCallback: ((NodeInstance) -> Unit) = {}
@Volatile
private var nodeCounts = mapOf<String, Int>()
@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<String, String> = 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<NodeInstanceRequest>) -> Unit): NetworkBuilder {
this.onNodeInstanceRequestedCallback = callback
return this
}
override fun onNodeInstance(callback: (NodeInstance) -> Unit): NetworkBuilder {
this.onNodeInstanceCallback = callback;
return this
}
override fun withNodeCounts(map: Map<String, Int>): 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<String, String>): NetworkBuilder {
this.backendOptions = HashMap(options)
return this
}
override fun onNodePushStart(callback: (BuiltNode) -> Unit): NetworkBuilder {
this.onNodePushStartCallback = callback;
return this;
}
override fun build(): CompletableFuture<Pair<List<NodeInstance>, 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 <T> List<CompletableFuture<T>>.toSingleFuture(): CompletableFuture<List<T>> {
return CompletableFuture.allOf(*this.toTypedArray()).thenApplyAsync {
this.map { it.getNow(null) }
}
}

View File

@ -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())
}
}
}

View File

@ -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
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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<String, Int> = hashMapOf()
@Option(names = arrayOf("--add", "-a"))
var nodesToAdd: MutableList<String> = arrayListOf()
fun isNew(): Boolean {
return nodesToAdd.isEmpty()
}
open fun backendOptions(): Map<String, String> {
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<Region> {
override fun convert(value: String): Region {
return regions[value] ?: throw Error("Unknown azure region: $value")
}
}
override fun backendOptions(): Map<String, String> {
return mapOf(Constants.REGION_ARG_NAME to region.name())
}
}

View File

@ -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<Int, Int> = emptyMap())

View File

@ -0,0 +1,18 @@
package net.corda.bootstrapper.containers.instance
import java.util.concurrent.CompletableFuture
interface Instantiator {
fun instantiateContainer(imageId: String,
portsToOpen: List<Int>,
instanceName: String,
env: Map<String, String>? = null): CompletableFuture<Pair<String, Map<Int, Int>>>
companion object {
val ADDITIONAL_NODE_INFOS_PATH = "/opt/corda/additional-node-infos"
}
fun getExpectedFQDN(instanceName: String): String
}

View File

@ -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<Int>,
instanceName: String,
env: Map<String, String>?): CompletableFuture<Pair<String, Map<Int, Int>>> {
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<Pair<String, Map<Int, Int>>>().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<ContainerGroup> {
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)
}
}

View File

@ -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<Int>,
instanceName: String,
env: Map<String, String>?): CompletableFuture<Pair<String, Map<Int, Int>>> {
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<String, String>?) =
(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)
}
}

View File

@ -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<String>
}

View File

@ -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<String> {
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<String>()
dockerClient.pushImageCmd("$privateRepoUrl:$networkName")
.withAuthConfig(dockerClient.authConfig())
.exec(object : ResultCallback<PushResponseItem> {
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)
}
}

View File

@ -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<String, String> {
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)
}
}

View File

@ -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<String> {
val dockerClient = DockerUtils.createLocalDockerClient()
dockerClient.tagImageCmd(localImageId, remoteImageName, networkName).withForce().exec()
return CompletableFuture.completedFuture("$remoteImageName:$networkName")
}
}

View File

@ -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<String, String> = emptyMap()) {
@Volatile
var safeNetworkName: String = networkName.replace(Constants.ALPHA_NUMERIC_ONLY_REGEX, "").toLowerCase()
@Volatile
var nodes: MutableMap<String, ConcurrentHashSet<PersistableNodeInstance>> = ConcurrentHashMap()
@Volatile
var networkInitiated: Boolean = false
@Volatile
var extraParams = ConcurrentHashMap<String, String>(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)
}
}

View File

@ -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()
}
}

View File

@ -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<NodeTemplateInfo> by fxid()
val templateChoiceBox: ChoiceBox<String> 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<String>() {
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<MouseEvent> { _ ->
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<Map<String, String>, 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>(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<FoundNode>, nodes: List<FoundNode>) ->
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<NodeType> = SimpleObjectProperty(type)
val localDockerImageId: SimpleStringProperty = SimpleStringProperty()
val repositoryImageId: SimpleStringProperty = SimpleStringProperty()
val status: SimpleObjectProperty<NodeBuildStatus> = SimpleObjectProperty(NodeBuildStatus.DISCOVERED)
val instances: MutableList<NodeInstanceEntry> = 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<FoundNodeTableEntry>()).observable()
val foundNotaries = Collections.synchronizedList(ArrayList<FoundNode>()).observable()
val networkContext = SimpleObjectProperty<Context>(null)
val unsortedNodes = Collections.synchronizedList(ArrayList<NodeTemplateInfo>()).observable()
val sortedNodes = SortedList(unsortedNodes, Comparator<NodeTemplateInfo> { 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<FoundNode>) {
foundNodes.clear()
nodesToAdd.forEach {
runLater {
foundNodes.add(FoundNodeTableEntry(it.name))
unsortedNodes.add(NodeTemplateInfo(it.name, NodeType.NODE))
}
}
}
fun notaries(notaries: List<FoundNode>) {
foundNotaries.clear()
notaries.forEach {
runLater {
foundNotaries.add(it)
unsortedNodes.add(NodeTemplateInfo(it.name, NodeType.NOTARY))
}
}
}
var baseDir = SimpleObjectProperty<File>(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<NodeInstanceRequest>) {
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)

View File

@ -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")
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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<InstanceInfo> {
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)
}
}
}

View File

@ -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()
}

View File

@ -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)
}
}

View File

@ -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<FoundNode> {
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)
}
}

View File

@ -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<Int, Int>) :
NodeInstanceRequest(
configFile,
baseDirectory,
copiedNodeConfig,
copiedNodeDir,
nodeConfig,
localImageId,
remoteImageName,
nodeInstanceName,
actualX500,
expectedFqName
) {
}

View File

@ -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<Int, Int>): NodeInstance {
return NodeInstance(configFile, baseDirectory, copiedNodeConfig, copiedNodeDir, nodeConfig, localImageId, remoteImageName, nodeInstanceName, actualX500, expectedFqName, reachableAddress, portMapping)
}
}

View File

@ -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<FoundNode, Int>): List<NodeInstanceRequest> {
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<InstanceInfo> {
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<NodeInstance> {
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<NodeInstance> {
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<Pair<String, Map<Int, Int>>> {
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<Pair<String, Map<Int, Int>>> {
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)
}
}

View File

@ -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<PushedNode> {
val localImageId = builtNode.localImageId
val nodeImageIdentifier = "node-${builtNode.name}"
val nodeImageNameFuture = containerPusher.pushContainerToImageRepository(localImageId,
nodeImageIdentifier, context.networkName)
return nodeImageNameFuture.thenApplyAsync { imageName -> builtNode.toPushedNode(imageName) }
}
}

View File

@ -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()}"
}
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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<FoundNode> {
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()
}
}

View File

@ -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)
)
}
}
}
}
}

View File

@ -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<CopiedNotary>)
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<Pair<File, File>>): NetworkParameters {
val notaryInfos = notaryFiles.map { (configFile, nodeInfoFile) ->
val validating = ConfigFactory.parseFile(configFile).getConfig("notary").getBoolean("validating")
nodeInfoFile.readBytes().deserialize<SignedNodeInfo>().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())
}
}
}

View File

@ -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<CopiedNotary>) {
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)
}
}

View File

@ -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<CopiedNotary>) {
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)
}
}

View File

@ -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"]

View File

@ -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

View File

@ -0,0 +1,3 @@
#!/bin/bash
sleep $1

View File

@ -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"]

View File

@ -0,0 +1,4 @@
rpcSettings {
address="0.0.0.0:10003"
adminAddress="127.0.0.1:10005"
}

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,3 @@
sshd {
port = 12222
}

View File

@ -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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.*?>
<VBox prefHeight="768.0" prefWidth="1024.0" xmlns="http://javafx.com/javafx/8.0.171" xmlns:fx="http://javafx.com/fxml/1">
<children>
<HBox alignment="CENTER_LEFT" minHeight="-Infinity" prefHeight="75.0" prefWidth="1073.0" styleClass="top-pane">
<children>
<ImageView fitHeight="150.0" fitWidth="100.0" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@cordalogo.png" />
</image>
</ImageView>
<Pane prefHeight="75.0" prefWidth="21.0" />
<Button mnemonicParsing="false" onAction="#onOpenClicked" text="Open nodes ..." />
<Button fx:id="buildButton" mnemonicParsing="false" text="Build">
<HBox.margin>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
</HBox.margin>
</Button>
<ToggleButton fx:id="localDockerBtn" mnemonicParsing="false" selected="true" text="Local Docker">
<toggleGroup>
<ToggleGroup fx:id="target" />
</toggleGroup>
</ToggleButton>
<ToggleButton fx:id="azureBtn" mnemonicParsing="false" text="Azure" toggleGroup="$target" />
<ChoiceBox fx:id="templateChoiceBox" prefWidth="150.0">
<HBox.margin>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
</HBox.margin>
</ChoiceBox>
<Button fx:id="addInstanceButton" mnemonicParsing="false" text="Add Instance">
<HBox.margin>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
</HBox.margin>
</Button>
</children>
</HBox>
<HBox maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" prefHeight="461.0" prefWidth="1014.0" VBox.vgrow="ALWAYS">
<children>
<SplitPane dividerPositions="0.6940371456500489" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" HBox.hgrow="ALWAYS">
<items>
<TableView fx:id="nodeTableView" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" prefHeight="458.0" prefWidth="662.0" />
<TextArea fx:id="infoTextArea" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" prefHeight="458.0" prefWidth="353.0" />
</items>
</SplitPane>
</children>
</HBox>
</children>
</VBox>